DHxWhy

Hotel Elevator Doors Image Gallery

Galleries 103

About

Hotel Elevator Doors Image Gallery for React and Tailwind CSS (shadcn/ui), with brushed-steel portrait doors, a lit floor display and a scene-filling Step inside zoom. It is built to make rooms and spaces feel like places to explore. The doors open only once each photo has loaded, the display counts every floor in between, and a two-column call panel lights the chosen floor. Includes controlled floor and inside state, callbacks, optional autoplay, keyboard controls, replaceable labels, reduced motion, and light and dark themes. Use it for hotel tours, property galleries, or portfolios. The included demo uses stock hotel photos. Requires React, with no animation library.

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Hotel Elevator Doors Image Gallery to your project and fits it to your theme.

Code

How Hotel Elevator Doors Image Gallery is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

"use client";

import * as React from "react";
import ElevatorDoorsGallery from "@/components/ui/elevator-doors-gallery";

// An imagined hotel, illustrated with individually sourced stock photography.
export const ELEVATOR_DEMO_ITEMS = [
  {
    src: "https://cdn.21st.dev/assets/mirror/43/43d4272e70e0353b5b91db831ddbc83b38b4bf4c504dd22fa0e0dd8ba5f1707f.jpg",
    alt: "The reception and seating area of a hotel lobby",
    floor: 1,
    title: "Lobby",
    caption: "Check in, breathe out.",
  },
  {
    src: "https://cdn.21st.dev/assets/mirror/0b/0bb94c6d946beae502cc3483376a8c697cd6b8b624be56b2199ea35594f42e40.jpg",
    alt: "Exercise equipment in a light-filled fitness center",
    floor: 5,
    title: "Fitness center",
    caption: "Sunrise sessions with a view.",
  },
  {
    src: "https://cdn.21st.dev/assets/mirror/3e/3e3113b502f7dc0cab9eff68b53b0f25d848bfce135ebb94f86f4240d306adcf.jpg",
    alt: "A made bed and warm furnishings in a hotel guest suite",
    floor: 12,
    title: "Guest suite",
    caption: "Your room, your pace.",
  },
  {
    src: "https://cdn.21st.dev/assets/mirror/d4/d4bf0449edfb95eea1ea2fc0fd8a6ffa6fbc82b0e17d33ec341908715201dc35.jpg",
    alt: "Softly lit seating in a hotel lounge bar",
    floor: 20,
    title: "Lounge bar",
    caption: "Cocktails after dark.",
  },
  {
    src: "https://cdn.21st.dev/assets/mirror/63/63e42ad51c42347f92776506d16610e142cd5f091ba38649065f5947ca00864d.jpg",
    alt: "Atmospheric lighting inside a club",
    floor: 28,
    title: "Club",
    caption: "The night goes up.",
  },
  {
    src: "https://cdn.21st.dev/assets/mirror/b9/b96f8f7b648279339e62ec6188611c4838473f8e48acbb4a5468d5d37b16d9a7.jpg",
    alt: "A rooftop swimming pool above the city",
    floor: 33,
    title: "Rooftop pool",
    caption: "Swim above the skyline.",
  },
];

export default function ElevatorDoorsGalleryDemo({ autoplay = true }: { autoplay?: boolean }) {
  const [floor, setFloor] = React.useState(0);
  const [inside, setInside] = React.useState(false);
  const [replay, setReplay] = React.useState(0);
  const [paused, setPaused] = React.useState(false);
  const [complete, setComplete] = React.useState(false);
  const [reduced, setReduced] = React.useState(false);

  React.useEffect(() => {
    const media = window.matchMedia("(prefers-reduced-motion: reduce)");
    const update = () => setReduced(media.matches);
    update();
    media.addEventListener("change", update);
    return () => media.removeEventListener("change", update);
  }, []);

  React.useEffect(() => {
    if (!autoplay || paused || complete || reduced) return;
    const enter = window.setTimeout(() => setInside(true), 4000);
    const leave = window.setTimeout(() => { setInside(false); setComplete(true); }, 6200);
    return () => { window.clearTimeout(enter); window.clearTimeout(leave); };
  }, [autoplay, paused, complete, reduced, replay]);

  const restart = () => {
    setFloor(0); setInside(false); setPaused(false); setComplete(false);
    setReplay((value) => value + 1);
  };

  return (
    <div data-elevator-demo className="flex min-h-svh w-full items-center justify-center bg-background px-5 py-3 text-foreground">
      <div className="w-full max-w-[560px]">
        <header className="mb-3 flex items-end justify-between gap-4 px-1">
          <div>
            <p className="mb-1.5 text-[10px] font-medium uppercase tracking-[0.2em] text-muted-foreground">Every floor, a different scene</p>
            <h2 className="text-[clamp(21px,3.4vw,27px)] font-medium leading-tight tracking-tight">Hotel elevator gallery</h2>
          </div>
          <button
            data-replay
            type="button"
            onClick={restart}
            className="mb-0.5 flex shrink-0 items-center gap-1.5 rounded-full border border-border px-3 py-2 text-xs text-muted-foreground outline-none hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
          >
            <svg aria-hidden="true" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7"><path d="M3 10a9 9 0 1 1 2.6 8.4M3 4v6h6" /></svg>
            Replay
          </button>
        </header>
        <ElevatorDoorsGallery
          key={replay}
          items={ELEVATOR_DEMO_ITEMS}
          floor={floor}
          onFloorChange={setFloor}
          inside={inside}
          onInsideChange={setInside}
          autoplay={autoplay && !paused && !complete && !reduced}
          initialDelay={1400}
          interval={20000}
          onPointerDownCapture={() => setPaused(true)}
          onKeyDownCapture={(event) => {
            if (["ArrowUp", "ArrowDown", "Home", "End", "Enter", " ", "Escape"].includes(event.key)) setPaused(true);
          }}
        />
        <footer className="mt-2 flex items-center justify-between gap-3 px-1 text-[10px] text-muted-foreground">
          <p>Six floors. Pick one, then step inside.<span className="ml-2 opacity-60">Demo</span></p>
          {autoplay && <button
            data-pause
            type="button"
            aria-pressed={paused || complete}
            onClick={() => { if (paused || complete) restart(); else setPaused(true); }}
            className="shrink-0 rounded px-1 py-1 outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring motion-reduce:hidden"
          >
            {complete ? "Replay tour" : paused ? "Resume tour" : "Pause tour"}
          </button>}
        </footer>
      </div>
    </div>
  );
}

More by DHxWhy

View all
  • Product Rail Gallery – Fashion Store Hero Section

    Product Rail Gallery – Fashion Store Hero Section

    1.4K
  • Sushi Conveyor Belt Menu – Restaurant Hero Section

    Sushi Conveyor Belt Menu – Restaurant Hero Section

    396
  • Sun Moon Dark Mode Theme Toggle

    Sun Moon Dark Mode Theme Toggle

    281
  • Team Lanyard Rail – Meet the Team Section with Flip Badges

    Team Lanyard Rail – Meet the Team Section with Flip Badges

    250
  • Skateboard Deck Rail Gallery – Skate Shop Hero Section

    Skateboard Deck Rail Gallery – Skate Shop Hero Section

    164
  • Live Notification Feed

    Live Notification Feed

    151
  • Country Flags Globe

    Country Flags Globe

    135
  • Airplane Window Gallery

    Airplane Window Gallery

    125

More in Galleries

View all
  • Thumbnail image viewer

    Thumbnail image viewer

    70
  • Animated Gallery

    Animated Gallery

    1.5K
  • Image Gallery

    Image Gallery

    128
  • Editorial Collage Hero

    Editorial Collage Hero

    460
  • 3D Parallax Unfurling Gallery

    3D Parallax Unfurling Gallery

    1K
  • Elastic Gallery

    Elastic Gallery

    978
  • Shared Element Gallery

    Shared Element Gallery

    448
  • Hero Section 6

    Hero Section 6

    902