# Intro Loader: Lookbook

> A season label instead of a number, and a tall frame for the looks. The last look lands as the half of the page beside the headline.

A shadcn/ui block from the [@keyframeui](https://keyframeui.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/keyframeui/intro-loader-lookbook-demo).

- Page, with a live preview: https://shadcn.exchange/r/keyframeui/intro-loader-lookbook-demo
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/keyframeui/intro-loader-lookbook-demo-light-d3dc185d64c2462a.png
- Registry item: https://keyframeui.com/r/intro-loader-lookbook-demo.json
- Categories: preloader, hero, image
- Listed: 2026-10-14, source last changed 2026-10-15

## Install

```bash
npx shadcn@latest add https://keyframeui.com/r/intro-loader-lookbook-demo.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `https://keyframeui.com/r/intro-loader.json`

## Notes from the registry

Live preview, examples and props: https://keyframeui.com/components/intro-loader#intro-loader-lookbook-demo

## Files

### `registry/demos/intro-loader-lookbook-demo.tsx`

```tsx
"use client";

import * as React from "react";

import { IntroLoader, IntroLoaderLine, IntroLoaderTarget } from "@/components/ui/intro-loader";

const look = (id: string, w: number, h: number) =>
  `https://images.unsplash.com/photo-${id}?auto=format&fit=crop&w=${w}&h=${h}&q=80`;

const hero = look("1581044777550-4cfa60707c03", 1100, 1400);
const looks = [
  look("1515886657613-9f3515b0c78f", 500, 750),
  look("1509631179647-0177331693ae", 500, 750),
  look("1529139574466-a303027c1d8b", 500, 750),
  look("1496747611176-843222e1e57c", 500, 750),
  hero,
];

// A season label in place of the number, a tall frame, and the last look
// landing as the half of the page beside the headline.
export function IntroLoaderLookbookDemo() {
  const [run, setRun] = React.useState(0);

  return (
    <div className="flex h-[var(--demo-height,36rem)] w-full flex-col overflow-hidden rounded-xl border border-stone-200 bg-[#efebe4] text-stone-900 dark:border-stone-800 dark:bg-stone-950 dark:text-stone-100">
      <BrowserBar url="maisonlune.com/ss27" run={run} onReload={() => setRun((r) => r + 1)} />
      <IntroLoader
        key={run}
        contained
        once={false}
        images={looks}
        brand={<span className="font-serif text-lg leading-6 italic">Maison Lune</span>}
        label="SS27"
        counterClassName="font-serif text-[clamp(3.5rem,13cqw,8rem)] font-normal tracking-[-0.02em] italic"
        frameClassName="aspect-[2/3] w-[clamp(7.5rem,17cqw,11rem)] rounded-none"
        className="@container min-h-0 flex-1"
      >
        {/* Stacked on narrow screens; the look beside the headline on wide ones. */}
        <div className="grid h-full grid-rows-[auto_minmax(0,1fr)_auto] @2xl:grid-cols-[minmax(0,1.1fr)_minmax(0,1fr)] @2xl:grid-rows-[auto_minmax(0,1fr)]">
          <header className="flex items-start justify-between gap-4 px-5 pt-5 sm:px-8 sm:pt-8">
            <span className="font-serif text-lg leading-6 italic">Maison Lune</span>
            <nav className="flex gap-5 text-[11px] leading-6 tracking-[0.16em] uppercase">
              {["Shop", "Lookbook", "Stores"].map((item) => (
                <IntroLoaderLine key={item} from="above" className={item === "Stores" ? "hidden @sm:block" : undefined}>
                  <a
                    href={`#${item.toLowerCase()}`}
                    className="text-stone-600 transition-colors duration-150 hover:text-stone-900 dark:text-stone-400 dark:hover:text-stone-100"
                  >
                    {item}
                  </a>
                </IntroLoaderLine>
              ))}
            </nav>
          </header>

          <div className="relative mt-5 min-h-0 @2xl:col-start-2 @2xl:row-span-2 @2xl:row-start-1 @2xl:mt-0">
            <IntroLoaderTarget>
              <img
                src={hero}
                alt="A woman in a pink ruffled dress standing in a dry summer field"
                className="absolute inset-0 size-full object-cover"
              />
            </IntroLoaderTarget>
          </div>

          <div className="min-w-0 px-5 pt-5 pb-6 sm:px-8 sm:pb-8 @2xl:col-start-1 @2xl:row-start-2 @2xl:self-end">
            <IntroLoaderLine className="text-[11px] tracking-[0.2em] text-stone-500 uppercase">
              Spring / Summer 2027
            </IntroLoaderLine>
            <h1 className="mt-3 font-serif text-[clamp(2.5rem,9.5cqw,7.5rem)] leading-[0.9] tracking-[-0.035em] @2xl:mt-4">
              <IntroLoaderLine>Summer,</IntroLoaderLine>
              <IntroLoaderLine className="italic">undone.</IntroLoaderLine>
            </h1>
            <IntroLoaderLine className="mt-5 hidden max-w-[17rem] text-sm leading-relaxed text-pretty text-stone-600 @2xl:block dark:text-stone-400">
              Washed linen, raw silk and cotton voile, cut loose for long days by the water.
            </IntroLoaderLine>
            <IntroLoaderLine className="mt-4 @2xl:mt-6">
              <a
                href="#shop"
                className="group inline-flex items-center gap-2 text-sm font-medium underline decoration-stone-400 underline-offset-4 transition-colors duration-150 hover:decoration-current"
              >
                Shop the collection
                <span aria-hidden className="transition-transform duration-200 group-hover:translate-x-0.5">
                  →
                </span>
              </a>
            </IntroLoaderLine>
          </div>
        </div>
      </IntroLoader>
    </div>
  );
}

function BrowserBar({ url, run, onReload }: { url: string; run: number; onReload: () => void }) {
  return (
    <div className="flex h-10 shrink-0 items-center gap-3 border-b border-stone-200 bg-white/60 px-3 dark:border-stone-800 dark:bg-stone-900/60">
      <div aria-hidden className="flex gap-1.5">
        {[0, 1, 2].map((dot) => (
          <span key={dot} className="size-2.5 rounded-full bg-stone-300 dark:bg-stone-700" />
        ))}
      </div>
      <button
        type="button"
        onClick={onReload}
        aria-label="Reload to play it again"
        title="Play again"
        className="grid size-7 place-items-center rounded-md text-stone-500 transition-[background-color,color,scale] duration-150 hover:bg-stone-200/70 hover:text-stone-900 active:scale-[0.94] dark:hover:bg-stone-800 dark:hover:text-stone-100"
      >
        <svg
          viewBox="0 0 16 16"
          fill="none"
          stroke="currentColor"
          strokeWidth="1.5"
          strokeLinecap="round"
          strokeLinejoin="round"
          className="size-3.5 transition-[rotate] duration-500 ease-[cubic-bezier(0.22,1,0.36,1)]"
          style={{ rotate: `${run * 360}deg` }}
        >
          <path d="M13.5 8a5.5 5.5 0 1 1-1.6-3.9" />
          <path d="M13.5 2.5v3h-3" />
        </svg>
      </button>
      <span className="mx-auto truncate rounded-md bg-stone-100 px-3 py-1 font-mono text-[11px] text-stone-500 dark:bg-stone-800/80 dark:text-stone-400">
        {url}
      </span>
      <span aria-hidden className="w-[4.75rem]" />
    </div>
  );
}
```
