DHxWhy

Hotel Elevator Doors Image Gallery

Galleries 93

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.1K
  • Sun Moon Dark Mode Theme Toggle

    Sun Moon Dark Mode Theme Toggle

    256
  • Live Notification Feed

    Live Notification Feed

    142
  • Country Flags Globe

    Country Flags Globe

    128
  • Airplane Window Gallery

    Airplane Window Gallery

    117
  • QR Code Scan Login Form with Password Fallback

    QR Code Scan Login Form with Password Fallback

    110
  • Domino Scroll Image Gallery

    Domino Scroll Image Gallery

    109
  • Ferris Wheel Image Gallery

    Ferris Wheel Image Gallery

    102

More in Galleries

View all
  • Animated Gallery

    Animated Gallery

    1.5K
  • Editorial Collage Hero

    Editorial Collage Hero

    458
  • Image Gallery

    Image Gallery

    1.4K
  • Thumbnail image viewer

    Thumbnail image viewer

    64
  • Elastic Gallery

    Elastic Gallery

    958
  • 3D Parallax Unfurling Gallery

    3D Parallax Unfurling Gallery

    1K
  • Img Sphere

    Img Sphere

    2.1K
  • Shared Element Gallery

    Shared Element Gallery

    446