DHxWhy

Airplane Window Gallery

Galleries 96

About

Airplane window image gallery for React and Tailwind CSS (shadcn/ui), with rising shades, changing destinations, and a full-bleed scenery zoom. It is built to let visitors fly out through the window and back to their seat. Two cabin windows show seat numbers, place names, and countries. Click a window to travel through its glass into a stage-filling landscape with a title, location, and caption; click anywhere, press Escape, or choose Sit back to return to the same seat. The seven-second guided demo reveals the scenery, changes a destination, flies out, and returns to the cabin. Includes Replay and Pause controls in the demo, two to five windows, configurable timing and scenery drift, controlled or click-driven viewing, semantic theme colors, and reduced-motion support. Use it for travel, airline, booking, and destination gallery pages. Automatic destination changes are opt-in in the reusable component.

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

Code

How Airplane Window 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 { AirplaneWindowGallery } from "@/components/ui/airplane-window-gallery";
import type { WindowItem } from "@/components/ui/airplane-window-gallery";

const settings = {
  windows: 2,
  autoplay: true,
  interval: 2100,
  drift: 18,
  shadeDuration: 400,
};

const VIEWS = [
  { id: "photo-1523632117739-12cc7fa95ad0", title: "South Malé Atoll", location: "Maldives", caption: "A sandbar curls through turquoise shallows." },
  { id: "photo-1636110330362-6f04cde3f1e4", title: "Mount Fuji", location: "Japan", caption: "The summit rises clean above the clouds." },
  { id: "photo-1615551043360-33de8b5f410c", title: "Grand Canyon", location: "United States", caption: "Layers of red rock and a winding river." },
  { id: "photo-1681407979901-566ba5099a0e", title: "Swiss Alps", location: "Switzerland", caption: "Snowy peaks seen from cruising altitude." },
  { id: "photo-1489516408517-0c0a15662682", title: "Dubai", location: "United Arab Emirates", caption: "The Palm and its crescent from above." },
  { id: "photo-1552561618-4fe029f7858a", title: "Glacial rivers", location: "Iceland", caption: "Meltwater braids across the valley floor." },
  { id: "photo-1789758612733-756075c4fb4c", title: "Great Barrier Reef", location: "Australia", caption: "Coral reefs scattered through clear blue water." },
  { id: "photo-1523374228107-6e44bd2b524e", title: "New York", location: "United States", caption: "The city lights up after dark." },
];

const ITEMS: WindowItem[] = VIEWS.map(({ id, title, location, caption }) => ({
  src: `https://images.unsplash.com/${id}?w=900&h=920&fit=crop&q=80&auto=format`,
  alt: `${title}, ${location}`,
  title,
  location,
  caption,
}));

const CCGATHER_URL = "https://ccgather.com/?utm_source=21st&utm_medium=component&utm_campaign=airplane_window_gallery";

type DemoProps = {
  windows?: number;
  autoplay?: boolean;
  interval?: number;
  drift?: number;
  shadeDuration?: number;
  /** Set false to explore the gallery without the one-time guided zoom. */
  tour?: boolean;
};

export default function Demo({ tour = true, ...props }: DemoProps = {}) {
  const s = { ...settings, ...props };
  const [activeWindow, setActiveWindow] = React.useState(null as number | null);
  const [reduced, setReduced] = React.useState(true);
  const [tourStopped, setTourStopped] = React.useState(false);
  const [paused, setPaused] = React.useState(false);
  const [replay, setReplay] = React.useState(0);
  const [step, setStep] = React.useState("reveal");
  const stopTour = () => setTourStopped(true);

  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 (!tour || !s.autoplay || reduced || tourStopped || paused) return;
    const change = window.setTimeout(() => setStep("change"), s.interval);
    // Let the first replacement shade finish lifting before looking through that window.
    const lookAt = s.interval + s.shadeDuration * 2 + 200;
    const look = window.setTimeout(() => { setActiveWindow(0); setStep("lookout"); }, lookAt);
    // 650ms to fly out, then 2.4 seconds outside before the 480ms return.
    const back = window.setTimeout(() => { setActiveWindow(null); setStep("returned"); }, lookAt + 650 + 2000);
    return () => { window.clearTimeout(change); window.clearTimeout(look); window.clearTimeout(back); };
  }, [tour, s.autoplay, s.interval, s.shadeDuration, reduced, tourStopped, paused, replay]);

  const restart = () => {
    setActiveWindow(null);
    setStep("reveal");
    setTourStopped(false);
    setPaused(false);
    setReplay(replay + 1);
  };

  return (
    <main
      data-airplane-demo
      data-tour={tour && s.autoplay && !reduced && !tourStopped && !paused}
      onPointerDownCapture={stopTour}
      onKeyDownCapture={stopTour}
      onFocusCapture={(event) => { if ((event.target as HTMLElement).matches("button, a")) stopTour(); }}
      className="flex min-h-screen w-full flex-col items-center justify-center gap-4 bg-background px-5 py-5 text-foreground"
    >
      <header className="text-center">
        <p className="mb-1 text-[10px] font-medium uppercase tracking-[.22em] text-muted-foreground">The window seat</p>
        <h1 className="text-2xl font-semibold tracking-tight">Airplane window gallery</h1>
        <p className="mt-1.5 text-xs text-muted-foreground">Lift the shade. Fly through the window. Return to your seat.</p>
      </header>
      <AirplaneWindowGallery
        key={replay}
        items={ITEMS}
        windows={s.windows}
        autoplay={s.autoplay && !paused}
        interval={s.interval}
        drift={paused ? 0 : s.drift}
        shadeDuration={s.shadeDuration}
        activeWindow={activeWindow}
        onActiveWindowChange={setActiveWindow}
        className="max-w-[640px]"
      />
      <div className="flex w-full max-w-[640px] flex-col items-center gap-2 text-center text-[11px] text-muted-foreground">
        <p data-tour-caption>{activeWindow !== null ? "Click anywhere to sit back." : step === "change" && !tourStopped ? "A new destination, behind the same window." : step === "returned" ? "Back at your window seat. Fly out again." : "Click any window to look out."}</p>
        <div className="flex items-center justify-center gap-3">
          <button type="button" data-replay onClick={restart} className="rounded-sm text-foreground underline decoration-border underline-offset-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">Replay preview</button>
          {s.autoplay && !reduced && <button type="button" data-pause aria-pressed={paused} onClick={() => { setPaused(!paused); setTourStopped(true); }} className="rounded-sm underline decoration-border underline-offset-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">{paused ? "Resume motion" : "Pause motion"}</button>}
          <span aria-hidden="true">·</span>
          <a href={CCGATHER_URL} target="_blank" rel="noreferrer" className="rounded-sm underline decoration-border underline-offset-4 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">By CCgather</a>
        </div>
      </div>
    </main>
  );
}

More by DHxWhy

View all
  • Sun Moon Dark Mode Theme Toggle

    Sun Moon Dark Mode Theme Toggle

    186
  • Live Notification Feed

    Live Notification Feed

    111
  • Country Flags Globe

    Country Flags Globe

    111
  • 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

    83
  • Constellation Image Gallery

    Constellation Image Gallery

    80
  • Ferris Wheel Image Gallery

    Ferris Wheel Image Gallery

    74
  • What's New Changelog Timeline

    What's New Changelog Timeline

    69

More in Galleries

View all
  • Thumbnail image viewer

    Thumbnail image viewer

    43
  • Animated Gallery

    Animated Gallery

    1.4K
  • File Upload

    File Upload

    18
  • Editorial Collage Hero

    Editorial Collage Hero

    431
  • 3D Parallax Unfurling Gallery

    3D Parallax Unfurling Gallery

    981
  • Image Gallery

    Image Gallery

    123
  • Elastic Gallery

    Elastic Gallery

    908
  • Tilted Grid Hero

    Tilted Grid Hero

    1.5K