DHxWhy

Hotel Elevator Doors Image Gallery

Galleries 60

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

    Sun Moon Dark Mode Theme Toggle

    186
  • Country Flags Globe

    Country Flags Globe

    110
  • Live Notification Feed

    Live Notification Feed

    109
  • Airplane Window Gallery

    Airplane Window Gallery

    95
  • Domino Scroll Image Gallery

    Domino Scroll Image Gallery

    93
  • QR Code Scan Login Form with Password Fallback

    QR Code Scan Login Form with Password Fallback

    82
  • Constellation Image Gallery

    Constellation Image Gallery

    80
  • Ferris Wheel Image Gallery

    Ferris Wheel Image Gallery

    73

More in Galleries

View all
  • Thumbnail image viewer

    Thumbnail image viewer

    43
  • Animated Gallery

    Animated Gallery

    1.4K
  • Hero Section 6

    Hero Section 6

    881
  • Editorial Collage Hero

    Editorial Collage Hero

    430
  • Layout Grid

    Layout Grid

    826
  • 3D Parallax Unfurling Gallery

    3D Parallax Unfurling Gallery

    981
  • Image Gallery

    Image Gallery

    90
  • File Upload

    File Upload

    18