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>
);
}















