Zum Inhalt springen
UI Kit
Accent

Parallax-Ebenen

Eine Szene aus Ebenen, die sich beim Scrollen unterschiedlich schnell bewegen und so Tiefe zeigen. Kleine Wege, auf dem Telefon halb so groß, und ohne JavaScript ruht jede Ebene im fertigen Bild.

Eine von 3 Components in Scroll-Effekte

4 Variants

Variants

Landschaft im Akzent

Eine gezeichnete Landschaft in fünf Ebenen, gefärbt im Akzent. Die Überschrift steht zwischen fernen Bergen und nahen Hügeln, der vordere Hügel wird zur Seite darunter.

Im Studio öffnen

Schwebende Karten

Ein Produktbildschirm mit Karten, die in unterschiedlicher Tiefe davor schweben. Je näher eine Karte, desto schneller, auf dem Telefon bleiben zwei.

Im Studio öffnen

Treibende Collage

Sechs Bilder treiben um eine mittige Botschaft, jedes in seinem Tempo. Auf dem Telefon bleiben vier, mit halbem Weg.

Im Studio öffnen

Wort, Bild, Text

Ein riesiges Wort weit hinten läuft gegen den Scroll, das Bild vorn am schnellsten, der Text dazwischen folgt. Drei Tiefen, eine ruhige Seite.

Im Studio öffnen

Details

Was Parallax-Ebenen mitbringt, aus Registry und Quellcode gelesen.

Bereich
Marketing
Variants
4
Version
1.0.1 · seit 1.1.0
Datei
components/ui/parallax-layers.tsx
Bringt mit
utilskitbuttoniconrevealsectionscroll-scene
npm-Pakete
Keine
Sprachen
Texte kommen über Props

Props

Die Schnittstelle ParallaxLayersProps aus components/ui/parallax-layers.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch. Nimmt außerdem alle Props von Omit<React.HTMLAttributes<HTMLElement>, 'title'> an.

  • variantoptional
    ParallaxLayersVariant

    landscapeproductcollagetype

  • eyebrowoptional
    string
  • titlePflicht
    React.ReactNode
  • descriptionoptional
    string
  • actionoptional
    { label: string; href: string }
  • mediaoptional
    React.ReactNode

    product: the screen in the middle; type: the picture in front

  • imagesoptional
    React.ReactNode[]

    collage: six pictures

  • cardsoptional
    FloatCard[]

    product: up to four cards floating in front of the screen

  • wordoptional
    string

    type: the giant word far behind

Barrierefreiheit

Aus dem Quellcode der Component abgeleitet.

  • Der Server rendert den Startzustand vollständig, JavaScript ergänzt nur die Interaktion.

Bewegung

  • Erscheint beim ersten Sichtkontakt mit einem kurzen Auftritt (kit-rise, useInView).

Quellcode

Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.

Datei anzeigen, 252 Zeilen
components/ui/parallax-layers.tsx
'use client';

import { useCallback, useEffect, useId, useRef } from 'react';
import { cn, round } from '@/lib/kit';
import { Eyebrow } from '@/components/ui/section';
import { Button } from '@/components/ui/button';
import { Icon, type IconName } from '@/components/ui/icon';
import { Reveal } from '@/components/ui/reveal';
import { useLive, useSceneProgress } from '@/components/ui/scroll-scene';

/*
 * A scene built from layers that move at different speeds while the page scrolls, so it reads as
 * depth. Every layer gets its own shift, written as a transform straight onto it; nothing in the
 * scene is repainted while scrolling. Speeds are small (24 to 220 px over the whole pass) and half
 * as large on phones.
 *
 * Without JavaScript and with reduced motion every layer rests in its middle position, which is
 * the composed picture.
 *
 *   landscape  a drawn landscape in five layers in the accent, the heading sits between them
 *   product    a product screen with cards floating in front of it at different depths
 *   collage    pictures drifting at their own pace around a centered message
 *   type       a giant word far behind, a picture in front, the text in between
 */

export type ParallaxLayersVariant = 'landscape' | 'product' | 'collage' | 'type';

export interface FloatCard {
  label: string;
  value: string;
  icon?: IconName;
}

export interface ParallaxLayersProps extends Omit<React.HTMLAttributes<HTMLElement>, 'title'> {
  variant?: ParallaxLayersVariant;
  eyebrow?: string;
  title: React.ReactNode;
  description?: string;
  action?: { label: string; href: string };
  /** product: the screen in the middle; type: the picture in front */
  media?: React.ReactNode;
  /** collage: six pictures */
  images?: React.ReactNode[];
  /** product: up to four cards floating in front of the screen */
  cards?: FloatCard[];
  /** type: the giant word far behind */
  word?: string;
}

type Els = Record<string, HTMLElement | null>;

/** Layer key and its shift in px over the whole pass (positive moves up while scrolling down) */
const SPEEDS: Record<ParallaxLayersVariant, [string, number][]> = {
  landscape: [['sky', 20], ['sun', 60], ['far', 90], ['head', 150], ['mid', 130], ['near', 190], ['front', 240]],
  product: [['head', 40], ['screen', 70], ['c0', 100], ['c1', 95], ['c2', 125], ['c3', 115]],
  collage: [['i0', 140], ['i1', 60], ['i2', 200], ['i3', 90], ['i4', 170], ['i5', 40], ['head', 20]],
  type: [['word', -60], ['media', 160], ['text', 60]],
};

/* ------------------------------------------------------------------ */
/* landscape: five drawn layers in the accent                          */
/* ------------------------------------------------------------------ */

const tone = (k: number) => `color-mix(in oklab, var(--primary) ${k}%, var(--background))`;

function Layer({ set, k, d, fill, className }: { set: (k: string) => (n: HTMLElement | null) => void; k: string; d: string; fill: string; className?: string }) {
  return (
    <div ref={set(k)} aria-hidden data-clip-ok className={cn('absolute inset-x-0 -bottom-[14%] top-0 will-change-transform', className)}>
      <svg viewBox="0 0 1600 900" preserveAspectRatio="xMidYMax slice" className="absolute inset-0 size-full">
        <path d={d} fill={fill} />
      </svg>
    </div>
  );
}

function Landscape({ set, eyebrow, title, description, action }: { set: (k: string) => (n: HTMLElement | null) => void } & Pick<ParallaxLayersProps, 'eyebrow' | 'title' | 'description' | 'action'>) {
  const id = useId().replace(/:/g, '');
  return (
    <div className="relative h-[min(100svh,58rem)] min-h-[34rem] overflow-hidden">
      <div ref={set('sky')} aria-hidden data-clip-ok className="absolute inset-x-0 -bottom-[14%] top-0 will-change-transform">
        <svg viewBox="0 0 1600 900" preserveAspectRatio="xMidYMax slice" className="absolute inset-0 size-full">
          <defs>
            <linearGradient id={`${id}s`} x1="0" y1="0" x2="0" y2="1">
              <stop offset="0" stopColor={tone(4)} />
              <stop offset="1" stopColor={tone(22)} />
            </linearGradient>
          </defs>
          <rect width="1600" height="900" fill={`url(#${id}s)`} />
        </svg>
      </div>
      <div ref={set('sun')} aria-hidden className="absolute inset-0 will-change-transform">
        <span className="absolute left-[74%] top-[5%] size-[clamp(4rem,12vw,11rem)] rounded-full lg:left-[80%] lg:top-[14%]" style={{ background: tone(55), boxShadow: `0 0 120px 40px ${tone(28)}` }} />
      </div>
      <Layer set={set} k="far" fill={tone(30)} d="M0 520L120 470L260 505L420 420L560 470L700 400L860 455L1000 380L1160 450L1300 410L1460 470L1600 430V900H0Z" />
      <div ref={set('head')} className="absolute inset-x-0 top-[13%] z-10 will-change-transform">
        <div className="kit-container text-center">
          <div className="mx-auto max-w-[44rem]">
            {eyebrow && <Eyebrow>{eyebrow}</Eyebrow>}
            <h2 className="text-balance text-display font-semibold leading-[1.02] tracking-[-0.035em] text-foreground">{title}</h2>
            {description && <p className="mx-auto mt-5 max-w-[32rem] text-pretty text-lead text-muted-foreground">{description}</p>}
            {action && (
              <div className="mt-8 flex justify-center">
                <div className="max-sm:w-full max-sm:[&>span]:w-full">
                  <Button href={action.href} icon="arrow-right" magnetic className="max-sm:w-full">
                    {action.label}
                  </Button>
                </div>
              </div>
            )}
          </div>
        </div>
      </div>
      {/* The hills from here on stand in front of the heading */}
      <Layer set={set} k="mid" className="z-20" fill={tone(42)} d="M0 640C140 590 260 600 380 560S640 600 760 570S1000 520 1140 560S1380 610 1600 560V900H0Z" />
      <Layer set={set} k="near" className="z-20" fill={tone(24)} d="M0 720C180 680 300 700 460 660S760 690 900 670S1200 640 1340 680S1520 700 1600 690V900H0Z" />
      <Layer set={set} k="front" className="z-20" fill="var(--background)" d="M0 800C200 770 380 790 560 765S900 780 1100 760S1420 790 1600 770V900H0Z" />
    </div>
  );
}

/* ------------------------------------------------------------------ */

/** Cards straddle the frame edges: the top pair over the window bar, the bottom pair below the screen, so they never sit on its content. Phones keep one card below the screen. */
const CARD_POS = ['left-[-2%] bottom-[-12%] sm:bottom-auto sm:left-[1%] sm:top-[-8%] lg:left-[-4%]', 'right-[1%] top-[-6%] lg:right-[-4%]', 'left-[5%] bottom-[-11%]', 'right-[6%] bottom-[-9%]'];

function Float({ card, className }: { card: FloatCard; className?: string }) {
  return (
    <div className={cn('flex items-center gap-3 rounded-xl border border-border bg-card px-4 py-3 shadow-[var(--highlight),var(--shadow-lg)]', className)}>
      <span className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-[color-mix(in_oklab,var(--primary)_14%,var(--card))] text-primary">
        <Icon name={card.icon ?? 'sparkle'} size={17} />
      </span>
      <span className="min-w-0">
        <span className="block text-[12px] text-muted-foreground">{card.label}</span>
        <span className="block text-[17px] font-semibold tabular-nums text-foreground">{card.value}</span>
      </span>
    </div>
  );
}

/** Around the message, never over it: the middle third stays free between the upper and lower band */
const COLLAGE_POS = [
  'left-[2%] top-[4%] w-[30%] aspect-[4/5] md:w-[22%]',
  'left-[33%] top-[-8%] w-[13%] aspect-square',
  'right-[2%] top-[6%] w-[28%] aspect-[3/4] md:w-[21%]',
  'left-[5%] bottom-[2%] w-[26%] aspect-square md:w-[17%]',
  'right-[5%] bottom-[-4%] w-[34%] aspect-[4/3] md:w-[24%]',
  'left-[48%] bottom-[-8%] w-[13%] aspect-[3/4]',
];

/* ------------------------------------------------------------------ */

/** A scene of layers that move at different speeds while the page scrolls. */
export function ParallaxLayers({ variant = 'landscape', eyebrow, title, description, action, media, images = [], cards = [], word, className, ...rest }: ParallaxLayersProps) {
  const live = useLive();
  const els = useRef<Els>({});
  const set = (k: string) => (n: HTMLElement | null) => {
    els.current[k] = n;
  };
  const apply = useCallback(
    (p: number) => {
      // Half the depth on phones, where the window is short and fast thumbs scroll far
      const f = window.innerWidth < 640 ? 0.5 : 1;
      for (const [k, speed] of SPEEDS[variant]) {
        const el = els.current[k];
        if (el) el.style.transform = `translate3d(0, ${round((0.5 - p) * speed * f, 1)}px, 0)`;
      }
    },
    [variant],
  );
  const scene = useSceneProgress<HTMLElement>(live, { mode: 'pass', self: false, onProgress: apply });
  // Back to the composed middle position when the scene stops (reduced motion switched on)
  useEffect(() => {
    if (live) return;
    for (const [k] of SPEEDS[variant]) els.current[k]?.style.removeProperty('transform');
  }, [live, variant]);

  const actionEl = action && (
    <div className="max-sm:w-full max-sm:[&>span]:w-full">
      <Button href={action.href} icon="arrow-right" magnetic className="max-sm:w-full">
        {action.label}
      </Button>
    </div>
  );

  return (
    <section {...rest} ref={scene} className={cn('relative overflow-hidden', variant !== 'landscape' && 'py-20 sm:py-28 lg:py-36', className)}>
      {variant === 'landscape' && <Landscape set={set} eyebrow={eyebrow} title={title} description={description} action={action} />}

      {variant === 'product' && (
        <div className="kit-container">
          <div ref={set('head')} className="relative z-10 mx-auto max-w-[44rem] text-center will-change-transform">
            <Reveal>
              {eyebrow && <Eyebrow>{eyebrow}</Eyebrow>}
              <h2 className="text-balance text-h1 font-semibold text-foreground">{title}</h2>
              {description && <p className="mx-auto mt-5 max-w-[34rem] text-pretty text-lead text-muted-foreground">{description}</p>}
              {actionEl && <div className="mt-8 flex justify-center">{actionEl}</div>}
            </Reveal>
          </div>
          <div className="relative mx-auto mt-14 max-w-[56rem] sm:mt-20">
            <div ref={set('screen')} className="will-change-transform">
              {media}
            </div>
            {cards.slice(0, 4).map((c, i) => (
              <div key={i} ref={set(`c${i}`)} className={cn('absolute z-10 will-change-transform', CARD_POS[i], i > 0 && 'hidden sm:block')}>
                <Float card={c} className="scale-[0.82] sm:scale-100" />
              </div>
            ))}
          </div>
        </div>
      )}

      {variant === 'collage' && (
        <div className="relative">
          <div aria-hidden data-clip-ok className="pointer-events-none absolute inset-0">
            {images.slice(0, 6).map((img, i) => (
              <div key={i} ref={set(`i${i}`)} className={cn('absolute will-change-transform', COLLAGE_POS[i], (i === 1 || i === 5) && 'hidden md:block')}>
                <div className="size-full overflow-hidden rounded-xl shadow-[var(--shadow-lg)] [&>*]:size-full">{img}</div>
              </div>
            ))}
          </div>
          <div ref={set('head')} className="kit-container relative z-10 flex min-h-[min(80svh,44rem)] items-center justify-center will-change-transform">
            <div className="max-w-[22rem] py-24 text-center sm:max-w-[30rem] md:py-0">
              {eyebrow && <Eyebrow>{eyebrow}</Eyebrow>}
              <h2 className="text-balance text-h2 font-semibold text-foreground">{title}</h2>
              {description && <p className="mx-auto mt-4 max-w-[28rem] text-pretty text-[15.5px] leading-relaxed text-muted-foreground">{description}</p>}
              {actionEl && <div className="mt-7 flex justify-center">{actionEl}</div>}
            </div>
          </div>
        </div>
      )}

      {variant === 'type' && (
        <div className="relative">
          <p ref={set('word') as never} aria-hidden className="pointer-events-none absolute inset-x-0 -top-[4%] select-none whitespace-nowrap text-center text-[clamp(6rem,22vw,22rem)] font-semibold leading-none tracking-[-0.06em] text-[color-mix(in_oklab,var(--foreground)_10%,transparent)] will-change-transform">
            {word}
          </p>
          <div className="kit-container relative grid grid-cols-1 items-center gap-10 md:grid-cols-[minmax(0,1.1fr)_minmax(0,1fr)] md:gap-16">
            <div ref={set('media')} className="will-change-transform">
              <div className="aspect-[4/5] overflow-hidden rounded-2xl shadow-[var(--shadow-lg)] sm:aspect-[5/6] [&>*]:size-full">{media}</div>
            </div>
            <div ref={set('text')} className="will-change-transform">
              {eyebrow && <Eyebrow>{eyebrow}</Eyebrow>}
              <h2 className="text-balance text-h2 font-semibold text-foreground">{title}</h2>
              {description && <p className="mt-5 max-w-[30rem] text-pretty text-lead text-muted-foreground">{description}</p>}
              {actionEl && <div className="mt-8">{actionEl}</div>}
            </div>
          </div>
        </div>
      )}
    </section>
  );
}

Häufige Fragen

Wie installiere ich Parallax-Ebenen?

Mit aiw add @ai-website/parallax-layers. Der Befehl legt die Datei components/ui/parallax-layers.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind button, icon, reveal, section und scroll-scene. Weitere npm-Pakete braucht sie nicht.

Welche Varianten hat Parallax-Ebenen?

4 Varianten, Landschaft im Akzent, Schwebende Karten, Treibende Collage und Wort, Bild, Text. Du wählst sie mit der Prop variant, zum Beispiel variant="product".

Kann ein KI-Assistent Parallax-Ebenen einbauen?

Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Parallax-Ebenen mit aiw_components und installieren sie mit aiw_add @ai-website/parallax-layers. Nenn im Prompt die Variante, etwa variant="landscape", dann schreibt der Assistent deine Inhalte direkt in die Props.

Kann ich Parallax-Ebenen ohne Code anpassen?

Ja, im Studio. Alle 4 Varianten lassen sich dort bearbeiten, Texte, Listen und Schalter. Das Studio schreibt den passenden ParallaxLayers-Code zum Kopieren.

Oft gesucht

Direkt zu

In die Zwischenablage kopiert