Vissza a webes elemekhez
Galériák és képek

Lapozható fotóalbum

Élő előnézet – a komponens teljes méretben.

Élő előnézet
HTML · CSS · JS
HTML
<section class="we-album" aria-label="Lapozható fotóalbum">

  <header class="we-album__intro">
    <span class="we-album__eyebrow">EMLÉKEK KÖNYVE</span>
    <h2>Helyek, ahová jó visszatérni</h2>
    <p>Egy kis utazás képekben — lapozz bele.</p>
  </header>

  <div class="we-album__stage">
    <div class="we-album__book"
         tabindex="0"
         aria-label="Fotóalbum. Lapozás a bal és jobb nyílbillentyűkkel.">

      <div class="we-album__left">
        <div class="we-album__photo">
          <img data-left-image
               src="https://images.unsplash.com/photo-1470770841072-f978cf4d019e?auto=format&amp;fit=crop&amp;w=900&amp;q=85"
               alt="Hegyek és tó">
        </div>
        <span class="we-album__page-number" data-left-number>01</span>
      </div>

      <div class="we-album__right">
        <div class="we-album__photo">
          <img data-right-image
               src="https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&amp;fit=crop&amp;w=900&amp;q=85"
               alt="Napfényes hegyvidék">
        </div>

        <div class="we-album__caption">
          <span data-caption-kicker>01 / UTAZÁS</span>
          <h3 data-caption-title>A csend pillanata</h3>
          <p data-caption-text>
            Van, amikor elég csak megállni, és körülnézni.
          </p>
        </div>

        <span class="we-album__page-number" data-right-number>02</span>

        <div class="we-album__turn-page" data-turn-page aria-hidden="true">
          <div class="we-album__turn-face we-album__turn-front">
            <img data-turn-front
                 src="https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&amp;fit=crop&amp;w=900&amp;q=85"
                 alt="">
          </div>
          <div class="we-album__turn-face we-album__turn-back">
            <img data-turn-back
                 src="https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?auto=format&amp;fit=crop&amp;w=900&amp;q=85"
                 alt="">
          </div>
        </div>
      </div>

    </div>
  </div>

  <nav class="we-album__controls" aria-label="Albumlapozás">
    <button class="we-album__nav"
            type="button"
            data-prev
            aria-label="Előző oldalpár">
      <span aria-hidden="true">←</span>
      <span>Előző</span>
    </button>

    <div class="we-album__progress" aria-live="polite">
      <span data-current>02</span>
      <i></i>
      <span data-total>08</span>
    </div>

    <button class="we-album__nav"
            type="button"
            data-next
            aria-label="Következő oldalpár">
      <span>Következő</span>
      <span aria-hidden="true">→</span>
    </button>
  </nav>

  <p class="we-album__hint">
    Lapozz a nyilakkal, vagy használd a billentyűzet bal és jobb nyilát.
  </p>

</section>
CSS
.we-album {
  --album-bg: #101b18;
  --album-paper: #f4f0e6;
  --album-ink: #25342d;
  --album-gold: #c7a76a;
  --album-muted: #b8c0b9;

  position: relative;
  isolation: isolate;
  width: 100%;
  box-sizing: border-box;
  padding: clamp(28px, 5vw, 64px) 20px 36px;
  overflow: hidden;
  color: #f4f0e6;
  background:
    radial-gradient(ellipse at 50% 40%, #263c31 0%, transparent 60%),
    var(--album-bg);
  font-family: Georgia, "Times New Roman", serif;
}

.we-album *,
.we-album *::before,
.we-album *::after {
  box-sizing: border-box;
}

.we-album__intro {
  position: relative;
  z-index: 1;
  max-width: 760px;
  margin: 0 auto 36px;
  text-align: center;
}

.we-album__eyebrow {
  display: inline-block;
  margin-bottom: 14px;
  color: var(--album-gold);
  font-family: Arial, sans-serif;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.28em;
}

.we-album__intro h2 {
  margin: 0;
  color: #f5f0e5;
  font-size: clamp(26px, 4vw, 44px);
  font-weight: 400;
  line-height: 1.2;
}

.we-album__intro p {
  margin: 14px 0 0;
  color: var(--album-muted);
  font-family: Arial, sans-serif;
  font-size: 15px;
  line-height: 1.7;
}

.we-album__stage {
  width: 100%;
  max-width: 1000px;
  margin: 0 auto;
  perspective: 1800px;
}

.we-album__book {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  width: 100%;
  max-width: 900px;
  margin: 0 auto;
  aspect-ratio: 1.45 / 1;
  min-height: 300px;
  background: #d7cbb1;
  border: 8px solid #26372e;
  border-radius: 5px;
  box-shadow:
    0 28px 65px rgb(0 0 0 / 35%),
    0 0 0 1px rgb(199 167 106 / 35%);
  transform-style: preserve-3d;
}

.we-album__book:focus-visible {
  outline: 2px solid var(--album-gold);
  outline-offset: 6px;
}

.we-album__left,
.we-album__right {
  position: relative;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  background: var(--album-paper);
  color: var(--album-ink);
}

.we-album__left {
  border-right: 1px solid rgb(55 44 28 / 20%);
  box-shadow: inset -12px 0 18px -18px #000;
}

.we-album__right {
  border-left: 1px solid rgb(55 44 28 / 15%);
  box-shadow: inset 12px 0 18px -18px #000;
}

.we-album__photo {
  position: absolute;
  inset: 0;
  overflow: hidden;
  background: #d8d4c8;
}

.we-album__photo img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.we-album__left .we-album__photo img {
  object-position: center;
}

.we-album__right .we-album__photo img {
  object-position: center;
}

.we-album__left::after,
.we-album__right::after {
  position: absolute;
  z-index: 1;
  inset: 0;
  content: "";
  pointer-events: none;
}

.we-album__left::after {
  background: linear-gradient(
    90deg,
    rgb(0 0 0 / 4%),
    transparent 35%,
    rgb(0 0 0 / 9%)
  );
}

.we-album__right::after {
  background: linear-gradient(
    90deg,
    rgb(0 0 0 / 9%),
    transparent 25%,
    rgb(0 0 0 / 3%)
  );
}

.we-album__page-number {
  position: absolute;
  z-index: 3;
  right: 18px;
  bottom: 13px;
  color: #5e665c;
  font-family: Arial, sans-serif;
  font-size: 11px;
  letter-spacing: 0.12em;
}

.we-album__caption {
  position: absolute;
  z-index: 2;
  right: 0;
  bottom: 0;
  left: 0;
  padding: 30px 24px 48px;
  color: #fff;
  background: linear-gradient(
    transparent,
    rgb(8 16 12 / 85%)
  );
}

.we-album__caption > span {
  color: #e5c98e;
  font-family: Arial, sans-serif;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.2em;
}

.we-album__caption h3 {
  margin: 10px 0 8px;
  color: #fff;
  font-size: clamp(19px, 2.5vw, 29px);
  font-weight: 400;
  line-height: 1.2;
}

.we-album__caption p {
  max-width: 330px;
  margin: 0;
  color: rgb(255 255 255 / 85%);
  font-family: Arial, sans-serif;
  font-size: 13px;
  line-height: 1.6;
}

/* A lap, amely átfordul a jobb oldalon */

.we-album__turn-page {
  position: absolute;
  z-index: 5;
  inset: 0;
  overflow: visible;
  pointer-events: none;
  transform: rotateY(0deg);
  transform-origin: left center;
  transform-style: preserve-3d;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}

.we-album__turn-page.is-turning-next {
  transform: rotateY(-180deg);
}

.we-album__turn-page.is-turning-prev {
  transform: rotateY(180deg);
}

.we-album__right.is-animating .we-album__turn-page {
  transition: transform 720ms cubic-bezier(.62, .02, .32, 1);
}

.we-album__turn-face {
  position: absolute;
  inset: 0;
  overflow: hidden;
  background: var(--album-paper);
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}

.we-album__turn-face img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.we-album__turn-back {
  transform: rotateY(180deg);
}

/* Lapozógombok */

.we-album__controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(18px, 4vw, 48px);
  margin: 28px auto 0;
}

.we-album__nav {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  min-height: 44px;
  padding: 10px 15px;
  border: 1px solid rgb(199 167 106 / 45%);
  border-radius: 2px;
  background: transparent;
  color: #f4f0e6;
  font-family: Arial, sans-serif;
  font-size: 13px;
  cursor: pointer;
  transition:
    color 180ms ease,
    background 180ms ease,
    border-color 180ms ease;
}

.we-album__nav span:first-child {
  font-size: 17px;
}

.we-album__nav:hover:not(:disabled) {
  border-color: var(--album-gold);
  background: var(--album-gold);
  color: #18221c;
}

.we-album__nav:focus-visible {
  outline: 2px solid var(--album-gold);
  outline-offset: 4px;
}

.we-album__nav:disabled {
  opacity: 0.35;
  cursor: not-allowed;
}

.we-album__progress {
  display: flex;
  align-items: center;
  gap: 12px;
  color: var(--album-gold);
  font-family: Arial, sans-serif;
  font-size: 12px;
  letter-spacing: 0.12em;
}

.we-album__progress i {
  display: block;
  width: 42px;
  height: 1px;
  background: rgb(199 167 106 / 55%);
}

.we-album__hint {
  margin: 20px auto 0;
  color: #aab4ac;
  font-family: Arial, sans-serif;
  font-size: 12px;
  line-height: 1.6;
  text-align: center;
}

@media (max-width: 600px) {
  .we-album {
    padding-right: 12px;
    padding-left: 12px;
  }

  .we-album__intro {
    margin-bottom: 24px;
  }

  .we-album__book {
    aspect-ratio: 0.95 / 1;
    min-height: 280px;
    border-width: 5px;
  }

  .we-album__caption {
    padding: 18px 10px 32px;
  }

  .we-album__caption h3 {
    font-size: 17px;
  }

  .we-album__caption p {
    font-size: 11px;
  }

  .we-album__page-number {
    right: 8px;
    bottom: 8px;
    font-size: 9px;
  }

  .we-album__controls {
    gap: 10px;
    margin-top: 20px;
  }

  .we-album__nav {
    gap: 5px;
    padding: 9px 10px;
    font-size: 12px;
  }

  .we-album__progress {
    gap: 6px;
    font-size: 11px;
  }

  .we-album__progress i {
    width: 18px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .we-album__right.is-animating .we-album__turn-page {
    transition-duration: 1ms;
  }

  .we-album__nav {
    transition: none;
  }
}
JavaScript
document.querySelectorAll(".we-album").forEach((album) => {
  // Az albumhoz tartozó elemek keresése.
  const $ = (selector) => album.querySelector(selector);

  const book = $(".we-album__book");
  const leftImage = $("[data-left-image]");
  const rightImage = $("[data-right-image]");
  const leftNumber = $("[data-left-number]");
  const rightNumber = $("[data-right-number]");
  const kicker = $("[data-caption-kicker]");
  const title = $("[data-caption-title]");
  const captionText = $("[data-caption-text]");
  const rightPage = $(".we-album__right");
  const turnPage = $("[data-turn-page]");
  const turnFront = $("[data-turn-front]");
  const turnBack = $("[data-turn-back]");
  const currentLabel = $("[data-current]");
  const totalLabel = $("[data-total]");
  const prevButton = $("[data-prev]");
  const nextButton = $("[data-next]");

  const requiredElements = {
    book,
    leftImage,
    rightImage,
    leftNumber,
    rightNumber,
    kicker,
    title,
    captionText,
    rightPage,
    turnPage,
    turnFront,
    turnBack,
    currentLabel,
    totalLabel,
    prevButton,
    nextButton
  };

  const missingElements = Object.entries(requiredElements)
    .filter(([, element]) => !element)
    .map(([name]) => name);

  if (missingElements.length) {
    console.error(
      "Fotóalbum: hiányzó HTML-elemek:",
      missingElements.join(", "),
      album
    );
    return;
  }

  // A fotóalbum tartalma.
  const photos = [
    {
      image: "https://images.unsplash.com/photo-1470770841072-f978cf4d019e?auto=format&fit=crop&w=900&q=85",
      alt: "Hegyek és tó",
      category: "UTAZÁS",
      title: "A csend pillanata",
      text: "Van, amikor elég csak megállni, és körülnézni."
    },
    {
      image: "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=900&q=85",
      alt: "Napfényes hegyvidéki táj",
      category: "TERMÉSZET",
      title: "A fény nyomában",
      text: "A táj minden órában más arcát mutatja."
    },
    {
      image: "https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?auto=format&fit=crop&w=900&q=85",
      alt: "Magas hegycsúcsok",
      category: "HEGYEK",
      title: "Közelebb az éghez",
      text: "Néha a legszebb út felfelé vezet."
    },
    {
      image: "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?auto=format&fit=crop&w=900&q=85",
      alt: "Napfényes erdei ösvény",
      category: "ERDŐ",
      title: "Az erdő titka",
      text: "A fák között még a csendnek is hangja van."
    },
    {
      image: "https://images.unsplash.com/photo-1473116763249-2faaef81ccda?auto=format&fit=crop&w=900&q=85",
      alt: "Zöldellő táj és hegyek",
      category: "KIRÁNDULÁS",
      title: "Ahol megpihen a szem",
      text: "Egy pillanat, amelyet jó eltenni emlékbe."
    },
    {
      image: "https://images.unsplash.com/photo-1501785888041-af3ef285b470?auto=format&fit=crop&w=900&q=85",
      alt: "Tó a hegyek között",
      category: "VIZEK",
      title: "Tükör a hegyek alatt",
      text: "A víz megőrzi az ég minden színét."
    },
    {
      image: "https://images.unsplash.com/photo-1518837695005-2083093ee35b?auto=format&fit=crop&w=900&q=85",
      alt: "Hullámok a tengerparton",
      category: "TENGER",
      title: "A hullámok meséje",
      text: "A parton ülve az idő is lassabban telik."
    },
    {
      image: "https://images.unsplash.com/photo-1470252649378-9c29740c9fa8?auto=format&fit=crop&w=900&q=85",
      alt: "Aranyló napfelkelte a táj felett",
      category: "NAPFÉNY",
      title: "Egy új reggel",
      text: "Minden nap tartogat valamit, amit érdemes észrevenni."
    }
  ];

  let spread = 0;
  let isAnimating = false;
  let animationTimer = null;
  let animationToken = 0;

  const duration = 740;
  const lastSpread = photos.length - 2;

  totalLabel.textContent = String(photos.length).padStart(2, "0");

  function setImage(imageElement, photo) {
    imageElement.src = photo.image;
    imageElement.alt = photo.alt;
  }

  function renderSpread() {
    const leftPhoto = photos[spread];
    const rightPhoto = photos[spread + 1];

    setImage(leftImage, leftPhoto);
    setImage(rightImage, rightPhoto);

    leftNumber.textContent = String(spread + 1).padStart(2, "0");
    rightNumber.textContent = String(spread + 2).padStart(2, "0");

    kicker.textContent =
      String(spread + 1).padStart(2, "0") +
      " / " +
      rightPhoto.category;

    title.textContent = rightPhoto.title;
    captionText.textContent = rightPhoto.text;

    currentLabel.textContent = String(spread + 2).padStart(2, "0");

    prevButton.disabled = isAnimating || spread === 0;
    nextButton.disabled =
      isAnimating || spread >= lastSpread;
  }

  function resetTurnPage() {
    turnPage.classList.remove(
      "is-turning-next",
      "is-turning-prev"
    );

    rightPage.classList.remove("is-animating");
    turnPage.style.transition = "none";
    turnPage.style.transform = "rotateY(0deg)";

    // A böngészőnek újra kell számolnia a kezdőállapotot,
    // mielőtt a következő lapfordítás elindul.
    void turnPage.offsetWidth;

    turnPage.style.removeProperty("transition");
    turnPage.style.removeProperty("transform");
  }

  function finishTurn(token, nextSpread) {
    if (token !== animationToken) {
      return;
    }

    window.clearTimeout(animationTimer);
    animationTimer = null;

    spread = nextSpread;
    isAnimating = false;

    resetTurnPage();
    renderSpread();
  }

  function turn(direction) {
    if (isAnimating) {
      return;
    }

    const nextSpread = spread + direction * 2;

    if (nextSpread < 0 || nextSpread > lastSpread) {
      return;
    }

    isAnimating = true;
    renderSpread();

    const currentRightPhoto = photos[spread + 1];
    const nextLeftPhoto = photos[nextSpread];
    const previousRightPhoto = photos[spread - 1];

    if (direction > 0) {
      setImage(turnFront, currentRightPhoto);
      setImage(turnBack, nextLeftPhoto);

      turnPage.classList.remove("is-turning-prev");
      rightPage.classList.add("is-animating");

      // A kezdőállapot visszaállítása az új animációhoz.
      turnPage.style.transition = "none";
      turnPage.style.transform = "rotateY(0deg)";
      void turnPage.offsetWidth;
      turnPage.style.removeProperty("transition");
      turnPage.style.removeProperty("transform");

      turnPage.classList.add("is-turning-next");
    } else {
      setImage(turnFront, currentRightPhoto);
      setImage(turnBack, previousRightPhoto);

      turnPage.classList.remove("is-turning-next");
      rightPage.classList.add("is-animating");

      turnPage.style.transition = "none";
      turnPage.style.transform = "rotateY(0deg)";
      void turnPage.offsetWidth;
      turnPage.style.removeProperty("transition");
      turnPage.style.removeProperty("transform");

      turnPage.classList.add("is-turning-prev");
    }

    const token = ++animationToken;

    // Biztonsági lezárás arra az esetre is, ha a
    // transitionend esemény nem fut le.
    animationTimer = window.setTimeout(() => {
      finishTurn(token, nextSpread);
    }, duration + 100);
  }

  nextButton.addEventListener("click", () => turn(1));
  prevButton.addEventListener("click", () => turn(-1));

  book.addEventListener("keydown", (event) => {
    if (event.key === "ArrowRight") {
      event.preventDefault();
      turn(1);
    } else if (event.key === "ArrowLeft") {
      event.preventDefault();
      turn(-1);
    }
  });

  turnPage.addEventListener("transitionend", (event) => {
    if (
      event.target !== turnPage ||
      event.propertyName !== "transform" ||
      !isAnimating
    ) {
      return;
    }

    const nextSpread =
      turnPage.classList.contains("is-turning-next")
        ? spread + 2
        : spread - 2;

    finishTurn(animationToken, nextSpread);
  });

  // Induló állapot.
  resetTurnPage();
  renderSpread();
});