Zum Inhalt springen
UI Kit
Accent

Stapelkarten

Karten, die sich beim Scrollen übereinander legen. Die nächste Karte schiebt sich über die letzte, die ein wenig zurücktritt. Das Stapeln ist reines CSS und funktioniert auch ohne JavaScript.

Eine von 4 Components in Scroll-Geschichten

5 Variants

Variants

Feature-Karten

Große Karten mit Text neben einem Bildschirm. Jede bleibt etwas tiefer stehen als die vorige, die verdeckte Karte wird kleiner und dunkler.

Im Studio öffnen

Ablauf mit Titelleisten

Schritte stapeln sich wie Reiter in einem Ordner. Die Titelleiste jedes Schritts bleibt über dem nächsten lesbar, der Inhalt blendet darunter aus.

Im Studio öffnen

Fallstudien

Ganze Bilder mit dem Ergebnis in großen Zahlen. Sie legen sich wie Abzüge übereinander, beim Darüberfahren zoomt das Bild leicht.

Im Studio öffnen

Überschrift und Stimmen

Die Überschrift bleibt links stehen, rechts stapeln sich Zitate mit einer Kennzahl. Auf dem Telefon steht die Überschrift zuerst, die Zitate stapeln sich darunter.

Im Studio öffnen

Stapel aus Notizen

Ein mittiger Stapel aus Notizen mit je einem Bild. Sie landen leicht gedreht wie Papier auf dem Tisch und treten zurück, wenn die nächste sie verdeckt.

Im Studio öffnen

Details

Was Stapelkarten mitbringt, aus Registry und Quellcode gelesen.

Bereich
Marketing
Variants
5
Version
1.0.1 · seit 1.1.0
Datei
components/ui/sticky-stack.tsx
Bringt mit
utilskitbuttoniconrevealsectionscroll-scene
npm-Pakete
Keine
Sprachen
Englisch und Deutsch, 3 eigene Texte

Props

Die Schnittstelle StickyStackProps aus components/ui/sticky-stack.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
    StickyStackVariant

    featuresprocesscase-studiessplitpile

  • eyebrowoptional
    string
  • titleoptional
    React.ReactNode
  • descriptionoptional
    string
  • itemsPflicht
    StackItem[]
  • actionoptional
    { label: string; href: string }

Eingebaute Texte

Diese Texte schreibt Stapelkarten selbst. Die Sprache folgt dem KitProvider, über die Prop labels ersetzt du einzelne.

Alle 3 Texte anzeigen
stepEnglisch StepDeutsch Schritt
ofEnglisch ofDeutsch von
readEnglisch Read the case studyDeutsch Fallstudie lesen

Barrierefreiheit

Aus dem Quellcode der Component abgeleitet.

  • Setzt aria-label, damit Screenreader Zweck und Zustand ansagen.
  • Zeigt bei Bedienung mit der Tastatur einen sichtbaren Fokusring.
  • 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).
  • Bewegt sich mit dem Scrollen, der Fortschritt folgt der Position auf der Seite.
  • Bringt eigene Animationen mit, ruhig getaktet und ohne Wackeln.
  • Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig. 2 Stellen im Code schalten Bewegung dann ab.

Quellcode

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

Datei anzeigen, 398 Zeilen
components/ui/sticky-stack.tsx
'use client';

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

/*
 * Cards that stack on top of each other while the page scrolls. Every card sticks a little lower
 * than the one before, the next one slides over it, and the covered card steps back (smaller,
 * dimmer). The stacking itself is CSS (position sticky), so it works without JavaScript too;
 * JavaScript only adds the stepping back. With reduced motion the cards stack without it.
 *
 *   features      large feature cards, text beside a picture
 *   process       steps that leave their title strip visible, like tabs in a folder
 *   case-studies  full pictures with the result in large figures
 *   split         a heading that stays on the left, quotes stacking on the right
 *   pile          a centered pile of notes that land slightly turned, like paper on a desk
 */

export type StickyStackVariant = 'features' | 'process' | 'case-studies' | 'split' | 'pile';

export interface StackItem {
  /** Small line above the title (category, client, step name) */
  eyebrow?: string;
  title: string;
  text?: string;
  /** Picture or drawing of the card */
  media?: React.ReactNode;
  /** A figure that carries the card, e.g. "+38%" (case-studies, process) */
  figure?: string;
  figureLabel?: string;
  /** Short facts as small chips */
  points?: string[];
  /** Person under a quote (split, pile) */
  author?: string;
  role?: string;
  avatar?: React.ReactNode;
  href?: string;
}

export interface StickyStackProps extends Omit<React.HTMLAttributes<HTMLElement>, 'title'> {
  variant?: StickyStackVariant;
  eyebrow?: string;
  title?: React.ReactNode;
  description?: string;
  items: StackItem[];
  action?: { label: string; href: string };
  labels?: Partial<(typeof TEXTS)['en']>;
}

const TEXTS = {
  en: { step: 'Step', of: 'of', read: 'Read the case study' },
  de: { step: 'Schritt', of: 'von', read: 'Fallstudie lesen' },
};

const pad = (n: number) => String(n).padStart(2, '0');

/**
 * How far the next card has slid over each card (0 to 1), measured from the cards themselves, so
 * any height and any sticky offset works. The result goes straight onto the marked elements of the
 * covered card, as transform and opacity: [data-back] steps back (scale by its value, keeps a
 * data-turn rotation), [data-veil] darkens, [data-fade] fades. No custom property on the card,
 * that would restyle the pictures in it on every frame.
 */
function useCover(live: boolean) {
  const list = useRef<HTMLOListElement>(null);
  useEffect(() => {
    const el = list.current;
    if (!el || !live) return;
    const cards = Array.from(el.querySelectorAll<HTMLElement>(':scope > li')).map((li) => ({
      li,
      back: li.querySelector<HTMLElement>('[data-back]'),
      veil: li.querySelector<HTMLElement>('[data-veil]'),
      fade: li.querySelector<HTMLElement>('[data-fade]'),
      last: -1,
    }));
    const base = cards.map((c) => c.back?.style.transform ?? '');
    // Sticky offsets and heights change only with the layout, so they are read on resize, not per frame
    let stick: number[] = [];
    let heights: number[] = [];
    const measure = () => {
      stick = cards.map((c) => parseFloat(getComputedStyle(c.li).top) || 0);
      heights = cards.map((c) => c.li.offsetHeight || 1);
    };
    let frame = 0;
    const update = () => {
      frame = 0;
      // All reads first, then all writes: no layout is forced between two cards
      const tops = cards.map((c) => c.li.getBoundingClientRect().top);
      for (let i = 0; i < cards.length - 1; i++) {
        // 0 while the next card is a full card height away from its place, 1 when it sits there
        const c = Math.round(Math.min(1, Math.max(0, 1 - (tops[i + 1] - stick[i + 1]) / heights[i])) * 1000) / 1000;
        const card = cards[i];
        if (c === card.last) continue;
        card.last = c;
        if (card.back) {
          const turn = Number(card.back.dataset.turn) || 0;
          card.back.style.transform = `${turn ? `rotate(${turn}deg) ` : ''}scale(${(1 - c * Number(card.back.dataset.back)).toFixed(4)})`;
        }
        if (card.veil) card.veil.style.opacity = (c * Number(card.veil.dataset.veil)).toFixed(3);
        if (card.fade) card.fade.style.opacity = (1 - c * Number(card.fade.dataset.fade)).toFixed(3);
      }
    };
    const onScroll = () => {
      if (!frame) frame = requestAnimationFrame(update);
    };
    const onResize = () => {
      measure();
      onScroll();
    };
    measure();
    update();
    window.addEventListener('scroll', onScroll, { passive: true });
    window.addEventListener('resize', onResize);
    return () => {
      cancelAnimationFrame(frame);
      window.removeEventListener('scroll', onScroll);
      window.removeEventListener('resize', onResize);
      cards.forEach((c, i) => {
        if (c.back) c.back.style.transform = base[i];
        c.veil?.style.removeProperty('opacity');
        c.fade?.style.removeProperty('opacity');
      });
    };
  }, [live]);
  return list;
}

/** The covered card steps back: smaller (by `back`) and under a calm veil. Only transform and opacity. */
const backProps = (back: number) => ({ 'data-back': back, style: { transformOrigin: '50% 0%' } as React.CSSProperties });

function Veil({ strength = 0.55 }: { strength?: number }) {
  return <span aria-hidden data-veil={strength} className="pointer-events-none absolute inset-0 z-10 rounded-[inherit] bg-background opacity-0" />;
}

function Header({ eyebrow, title, description, action, center }: Pick<StickyStackProps, 'eyebrow' | 'title' | 'description' | 'action'> & { center?: boolean }) {
  if (!title && !eyebrow) return null;
  return (
    <Reveal className={cn('mb-12 max-w-[44rem] sm:mb-16', center && 'mx-auto text-center')}>
      {eyebrow && <Eyebrow>{eyebrow}</Eyebrow>}
      {title && <h2 className="text-balance text-h2 font-semibold text-foreground">{title}</h2>}
      {description && <p className={cn('mt-5 max-w-[36rem] text-pretty text-lead text-muted-foreground', center && 'mx-auto')}>{description}</p>}
      {action && (
        <div className={cn('mt-8', center && 'flex justify-center')}>
          <Button href={action.href} variant="secondary" icon="arrow-right">
            {action.label}
          </Button>
        </div>
      )}
    </Reveal>
  );
}

/* ------------------------------------------------------------------ */
/* features                                                            */
/* ------------------------------------------------------------------ */

function Features({ items, live }: { items: StackItem[]; live: boolean }) {
  const list = useCover(live);
  return (
    <ol ref={list} className="grid grid-cols-1 gap-6 sm:gap-8">
      {items.map((it, i) => (
        <li key={i} className="sticky" style={{ top: `calc(clamp(1rem, 6svh, 4.5rem) + ${i} * 0.75rem)`, zIndex: i + 1 }}>
          <article className="relative overflow-hidden rounded-2xl border border-border bg-card shadow-[var(--highlight),var(--shadow-md)] will-change-transform" {...backProps(0.05)}>
            <Veil />
            <div className="grid grid-cols-1 lg:grid-cols-[minmax(0,1fr)_minmax(0,1.1fr)]">
              <div className="flex flex-col justify-between gap-8 p-6 sm:p-8 lg:p-12">
                <div>
                  <p className="flex items-center gap-3 text-[13px] font-medium text-muted-foreground">
                    <span className="tabular-nums text-[color-mix(in_oklab,var(--primary)_80%,var(--foreground))]">{pad(i + 1)}</span>
                    <span aria-hidden className="h-px w-8 bg-border-strong" />
                    {it.eyebrow}
                  </p>
                  <h3 className="mt-5 text-balance text-[clamp(1.5rem,1.1rem+1.2vw,2.125rem)] font-semibold leading-[1.15] tracking-[-0.02em] text-foreground">
                    {/* With href the title is the link of the card */}
                    {it.href ? (
                      <a href={it.href} className="group/title inline-flex items-center gap-2 rounded-md outline-none transition-colors hover:text-[color-mix(in_oklab,var(--primary)_70%,var(--foreground))] focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-4 focus-visible:outline-[var(--ring)]">
                        {it.title}
                        <Icon name="arrow-right" size={20} className="shrink-0 transition-transform duration-300 ease-[var(--ease-kit)] group-hover/title:translate-x-1 motion-reduce:transition-none" />
                      </a>
                    ) : (
                      it.title
                    )}
                  </h3>
                  {it.text && <p className="mt-4 max-w-[30rem] text-pretty text-[15.5px] leading-relaxed text-muted-foreground">{it.text}</p>}
                </div>
                {it.points && it.points.length > 0 && (
                  <ul className="flex flex-wrap gap-2">
                    {it.points.map((pt) => (
                      <li key={pt} className="inline-flex items-center gap-1.5 rounded-full border border-border bg-surface px-3 py-1.5 text-[12.5px] text-foreground">
                        <Icon name="check" size={13} className="text-primary" />
                        {pt}
                      </li>
                    ))}
                  </ul>
                )}
              </div>
              {it.media && (
                <div className="relative min-h-[13rem] overflow-hidden border-t border-border bg-surface sm:min-h-[18rem] lg:min-h-[24rem] lg:border-l lg:border-t-0 [&>*]:absolute [&>*]:inset-0 [&>*]:size-full">
                  {it.media}
                </div>
              )}
            </div>
          </article>
        </li>
      ))}
    </ol>
  );
}

/* ------------------------------------------------------------------ */
/* process                                                             */
/* ------------------------------------------------------------------ */

function Process({ items, live, t }: { items: StackItem[]; live: boolean; t: (typeof TEXTS)['en'] }) {
  const list = useCover(live);
  return (
    <ol ref={list} className="grid grid-cols-1 gap-4">
      {items.map((it, i) => (
        // Every step sticks one title strip lower, so the titles of the steps before stay readable
        <li key={i} className="sticky" style={{ top: `calc(clamp(1rem, 5svh, 3.5rem) + ${i} * 3.5rem)`, zIndex: i + 1 }}>
          <article className="relative overflow-hidden rounded-2xl border border-border bg-card shadow-[var(--highlight),var(--shadow-sm)]" aria-label={`${t.step} ${i + 1} ${t.of} ${items.length}`}>
            <header className="flex h-14 items-center gap-4 border-b border-border px-5 sm:px-8">
              <span className="flex size-7 shrink-0 items-center justify-center rounded-full bg-primary text-[12px] font-semibold tabular-nums text-primary-foreground">{i + 1}</span>
              <h3 className="min-w-0 flex-1 truncate text-[15px] font-semibold text-foreground">{it.title}</h3>
              {it.eyebrow && <span className="shrink-0 text-[12.5px] tabular-nums text-muted-foreground">{it.eyebrow}</span>}
            </header>
            {/* The body fades under the next step, the strip above stays sharp */}
            <div className="relative grid grid-cols-1 gap-6 p-5 sm:p-8 md:grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)] md:gap-10 lg:min-h-[17rem] lg:p-10" data-fade={0.85}>
              <div>
                {it.text && <p className="max-w-[34rem] text-pretty text-[15.5px] leading-relaxed text-muted-foreground">{it.text}</p>}
                {it.points && it.points.length > 0 && (
                  <ul className="mt-6 grid grid-cols-1 gap-2.5 sm:grid-cols-2">
                    {it.points.map((pt) => (
                      <li key={pt} className="flex items-start gap-2.5 text-[14px] text-foreground">
                        <Icon name="check-circle" size={16} className="mt-0.5 shrink-0 text-primary" />
                        {pt}
                      </li>
                    ))}
                  </ul>
                )}
              </div>
              {(it.figure || it.media) && (
                <div className="flex min-h-[8rem] flex-col justify-end rounded-xl border border-border bg-surface p-5 lg:p-7">
                  {it.media}
                  {it.figure && <p className="text-[clamp(2rem,1.5rem+1.6vw,3rem)] font-semibold leading-none tracking-[-0.03em] tabular-nums text-foreground">{it.figure}</p>}
                  {it.figureLabel && <p className="mt-2 text-[13px] text-muted-foreground">{it.figureLabel}</p>}
                </div>
              )}
            </div>
          </article>
        </li>
      ))}
    </ol>
  );
}

/* ------------------------------------------------------------------ */
/* case-studies                                                        */
/* ------------------------------------------------------------------ */

function Cases({ items, live, t }: { items: StackItem[]; live: boolean; t: (typeof TEXTS)['en'] }) {
  const list = useCover(live);
  return (
    <ol ref={list} className="grid grid-cols-1 gap-6">
      {items.map((it, i) => (
        <li key={i} className="sticky" style={{ top: `calc(clamp(1rem, 6svh, 4rem) + ${i} * 0.625rem)`, zIndex: i + 1 }}>
          <article className="dark group relative isolate h-[min(34rem,74svh)] overflow-hidden rounded-2xl border border-border bg-bezel text-foreground shadow-[var(--shadow-lg)] will-change-transform sm:h-[min(36rem,72svh)]" {...backProps(0.06)}>
            <Veil strength={0.6} />
            <div className="absolute inset-0 -z-10 transition-transform duration-700 ease-[var(--ease-kit)] group-hover:scale-[1.03] motion-reduce:transition-none [&>*]:size-full">{it.media}</div>
            <span aria-hidden className="absolute inset-0 -z-10 bg-[linear-gradient(to_top,color-mix(in_oklab,var(--background)_94%,transparent)_0%,color-mix(in_oklab,var(--background)_86%,transparent)_55%,color-mix(in_oklab,var(--background)_62%,transparent)_74%,transparent_100%)] lg:bg-[linear-gradient(to_top,color-mix(in_oklab,var(--background)_92%,transparent)_0%,color-mix(in_oklab,var(--background)_55%,transparent)_45%,transparent_75%)]" />
            <div className="flex h-full flex-col justify-end gap-6 p-6 sm:p-10 lg:flex-row lg:items-end lg:justify-between lg:p-12">
              <div className="max-w-[34rem]">
                {it.eyebrow && <p className="text-[13px] font-medium text-[color-mix(in_oklab,var(--primary)_40%,var(--foreground))]">{it.eyebrow}</p>}
                <h3 className="mt-3 text-balance text-[clamp(1.5rem,1.1rem+1.3vw,2.25rem)] font-semibold leading-[1.12] tracking-[-0.02em]">{it.title}</h3>
                {it.text && <p className="mt-3 max-w-[30rem] text-pretty text-[15px] leading-relaxed text-muted-foreground">{it.text}</p>}
                {it.href && (
                  <a href={it.href} className="mt-6 inline-flex min-h-11 items-center gap-2 rounded-full text-[14px] font-medium text-foreground underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-4 focus-visible:outline-primary">
                    {t.read}
                    <Icon name="arrow-right" size={16} className="transition-transform duration-300 group-hover:translate-x-0.5" />
                  </a>
                )}
              </div>
              {it.figure && (
                <div className="shrink-0 border-t border-[color-mix(in_oklab,var(--foreground)_20%,transparent)] pt-5 lg:border-l lg:border-t-0 lg:pl-8 lg:pt-0">
                  <p className="text-[clamp(2.75rem,2rem+3vw,5rem)] font-semibold leading-none tracking-[-0.04em] tabular-nums">{it.figure}</p>
                  {it.figureLabel && <p className="mt-2 text-[13.5px] text-muted-foreground">{it.figureLabel}</p>}
                </div>
              )}
            </div>
          </article>
        </li>
      ))}
    </ol>
  );
}

/* ------------------------------------------------------------------ */
/* split                                                               */
/* ------------------------------------------------------------------ */

function Quote({ it }: { it: StackItem }) {
  return (
    <figure className="flex h-full flex-col justify-between gap-8 p-6 sm:p-8">
      <div>
        <Icon name="quote" size={22} className="text-[color-mix(in_oklab,var(--primary)_75%,var(--muted-foreground))]" />
        <blockquote className="mt-5 text-pretty text-[clamp(1.125rem,0.95rem+0.6vw,1.5rem)] font-medium leading-snug tracking-[-0.01em] text-foreground">{it.title}</blockquote>
      </div>
      <figcaption className="flex items-center gap-3">
        {it.avatar && <span className="size-10 shrink-0 overflow-hidden rounded-full [&>*]:size-full">{it.avatar}</span>}
        <span className="min-w-0">
          <span className="block truncate text-[14px] font-semibold text-foreground">{it.author}</span>
          {it.role && <span className="block truncate text-[13px] text-muted-foreground">{it.role}</span>}
        </span>
        {it.figure && (
          <span className="ml-auto shrink-0 text-right">
            <span className="block text-[18px] font-semibold tabular-nums text-foreground">{it.figure}</span>
            {it.figureLabel && <span className="block text-[12px] text-muted-foreground">{it.figureLabel}</span>}
          </span>
        )}
      </figcaption>
    </figure>
  );
}

function Split({ items, live, head }: { items: StackItem[]; live: boolean; head: React.ReactNode }) {
  const list = useCover(live);
  return (
    <div className="grid grid-cols-1 gap-10 lg:grid-cols-[minmax(0,0.85fr)_minmax(0,1.15fr)] lg:gap-16">
      <div className="lg:sticky lg:top-[clamp(2rem,12svh,7rem)] lg:self-start [&>div]:mb-0">{head}</div>
      <ol ref={list} className="grid grid-cols-1 gap-5">
        {items.map((it, i) => (
          <li key={i} className="sticky" style={{ top: `calc(clamp(1rem, 12svh, 7rem) + ${i} * 0.875rem)`, zIndex: i + 1 }}>
            <article className="relative min-h-[16rem] overflow-hidden rounded-2xl lg:min-h-[20rem] border border-border bg-card shadow-[var(--highlight),var(--shadow-md)] will-change-transform" {...backProps(0.04)}>
              <Veil />
              <Quote it={it} />
            </article>
          </li>
        ))}
      </ol>
    </div>
  );
}

/* ------------------------------------------------------------------ */
/* pile                                                                */
/* ------------------------------------------------------------------ */

const TURN = [-2.2, 1.6, -1.1, 2.4, -1.8, 1.2];

function Pile({ items, live }: { items: StackItem[]; live: boolean }) {
  const list = useCover(live);
  return (
    <ol ref={list} className="mx-auto grid max-w-[38rem] grid-cols-1 gap-10">
      {items.map((it, i) => (
        <li key={i} className="sticky" style={{ top: `calc(clamp(1.5rem, 16svh, 9rem) + ${i} * 0.375rem)`, zIndex: i + 1 }}>
          {/* Each note lands turned a little, so the pile looks laid by hand. */}
          <article
            className="relative overflow-hidden rounded-2xl border border-border bg-card shadow-[var(--highlight),var(--shadow-lg)] will-change-transform"
            data-back={0.04}
            data-turn={TURN[i % TURN.length]}
            style={{ transform: `rotate(${TURN[i % TURN.length]}deg)` }}
          >
            <Veil strength={0.45} />
            {it.media && <div className="relative h-40 overflow-hidden border-b border-border bg-surface sm:h-48 [&>*]:size-full">{it.media}</div>}
            <Quote it={it} />
          </article>
        </li>
      ))}
    </ol>
  );
}

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

/** Cards that stack while the page scrolls, five layouts. */
export function StickyStack({ variant = 'features', eyebrow, title, description, items, action, labels, className, ...rest }: StickyStackProps) {
  const t = useTexts(TEXTS, labels);
  const live = useLive();
  const head = <Header eyebrow={eyebrow} title={title} description={description} action={action} center={variant === 'pile'} />;
  return (
    // overflow-x-clip, not hidden: hidden would stop the cards from sticking
    <section {...rest} className={cn('relative overflow-x-clip py-20 sm:py-24 lg:py-32', className)}>
      <div className="kit-container">
        {variant !== 'split' && head}
        {variant === 'features' && <Features items={items} live={live} />}
        {variant === 'process' && <Process items={items} live={live} t={t} />}
        {variant === 'case-studies' && <Cases items={items} live={live} t={t} />}
        {variant === 'split' && <Split items={items} live={live} head={head} />}
        {variant === 'pile' && <Pile items={items} live={live} />}
      </div>
    </section>
  );
}

Häufige Fragen

Wie installiere ich Stapelkarten?

Mit aiw add @ai-website/sticky-stack. Der Befehl legt die Datei components/ui/sticky-stack.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 Stapelkarten?

5 Varianten, Feature-Karten, Ablauf mit Titelleisten, Fallstudien, Überschrift und Stimmen und Stapel aus Notizen. Du wählst sie mit der Prop variant, zum Beispiel variant="process".

Kann ich die Texte von Stapelkarten übersetzen?

Ja. Stapelkarten bringt 3 eigene Texte auf Deutsch und Englisch mit, etwa „Schritt“, „von“ und „Fallstudie lesen“. Die Sprache folgt dem KitProvider, einzelne Texte ersetzt du über die Prop labels.

Kann ein KI-Assistent Stapelkarten einbauen?

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

Kann ich Stapelkarten ohne Code anpassen?

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

Oft gesucht

Direkt zu

In die Zwischenablage kopiert