Seitlich scrollen
Eine Section, die seitlich wandert, während die Seite nach unten scrollt. Sie bleibt stehen, bis ihre Reihe vorbeigezogen ist, Linie und Zähler zeigen den Stand. Auf dem Telefon hat jede Variante ihr eigenes Layout, ohne JavaScript scrollt die Reihe von Hand.
Eine von 4 Components in Scroll-Geschichten
Variants
Details
Was Seitlich scrollen mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Scroll-Geschichten
- Bereich
- Marketing
- Variants
- 5
- Version
- 1.0.0 · seit 1.1.0
- Datei
- components/ui/sideways-scroll.tsx
- npm-Pakete
- Keine
- Sprachen
- Englisch und Deutsch, 4 eigene Texte
- Mit KI bauen
- Per MCP in Claude, Cursor, Copilot und Codex
Props
Die Schnittstelle SidewaysScrollProps aus components/ui/sideways-scroll.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch. Nimmt außerdem alle Props von Omit<React.HTMLAttributes<HTMLElement>, 'title'> an.
variantoptionalSidewaysScrollVariantcardstimelinegallerychaptersproducts
eyebrowoptionalstringtitleoptionalReact.ReactNodedescriptionoptionalstringitemsPflichtSidewaysItem[]actionoptional{ label: string; href: string }backdropoptionalstringgallery: a large word behind the pictures
Eingebaute Texte
Diese Texte schreibt Seitlich scrollen selbst. Die Sprache folgt dem KitProvider, über die Prop labels ersetzt du einzelne.
Alle 4 Texte anzeigen
| Schlüssel | Englisch | Deutsch |
|---|---|---|
| region | Englisch Scrollable row | Deutsch Seitlich scrollbare Reihe |
| of | Englisch of | Deutsch von |
| more | Englisch Learn more | Deutsch Mehr erfahren |
| all | Englisch See the whole collection | Deutsch Ganze Kollektion ansehen |
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- Setzt aria-label, damit Screenreader Zweck und Zustand ansagen.
- Nutzt die Rolle running.
- Ergänzt Text für Screenreader, wo Icons und Grafiken allein stehen.
- 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).
- Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig. 3 Stellen im Code schalten Bewegung dann ab.
Quellcode
Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.
Datei anzeigen, 395 Zeilen
'use client';
import { useCallback, useEffect, useRef, useState } from 'react';
import { cn, round, useTexts } 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 { useScrollFade } from '@/components/ui/motion';
import { seg, useLive, useSceneProgress } from '@/components/ui/scroll-scene';
/*
* A section that moves sideways while the page scrolls down. The section sticks for as long as
* its row needs to pass, one pixel of scrolling moves the row by one pixel, a line and a counter
* show how far you are. From 768 px on. On phones every variant has its own layout (a list, a
* timeline downwards or a swipe row). Without JavaScript and with reduced motion the row is a
* normal row you scroll sideways with touch, trackpad or keyboard.
*
* cards service or feature cards with an icon and a number
* timeline years on a line that fills while you pass them
* gallery pictures of different widths with a large word gliding slower behind them
* chapters wide panels, one chapter each, text beside a picture
* products a shop row with prices and a closing tile to the full collection
*/
export type SidewaysScrollVariant = 'cards' | 'timeline' | 'gallery' | 'chapters' | 'products';
export interface SidewaysItem {
title: string;
text?: string;
/** Small line above the title: category, place, a date */
eyebrow?: string;
icon?: IconName;
media?: React.ReactNode;
/** timeline: the year or date in large figures */
year?: string;
/** products: the price as written, e.g. "€34" */
price?: string;
/** products: a short label on the picture, e.g. "New" */
tag?: string;
href?: string;
}
export interface SidewaysScrollProps extends Omit<React.HTMLAttributes<HTMLElement>, 'title'> {
variant?: SidewaysScrollVariant;
eyebrow?: string;
title?: React.ReactNode;
description?: string;
items: SidewaysItem[];
action?: { label: string; href: string };
/** gallery: a large word behind the pictures */
backdrop?: string;
labels?: Partial<(typeof TEXTS)['en']>;
}
const TEXTS = {
en: { region: 'Scrollable row', of: 'of', more: 'Learn more', all: 'See the whole collection' },
de: { region: 'Seitlich scrollbare Reihe', of: 'von', more: 'Mehr erfahren', all: 'Ganze Kollektion ansehen' },
};
const pad = (n: number) => String(n).padStart(2, '0');
/** Where an item counts as reached: its left edge passes this share of the window width */
const LINE = 0.42;
interface Mark {
a: number;
b: number;
}
/**
* Measures the row once it is live (and on resize): how far it moves and at which progress each
* item reaches the reading line. No measuring while scrolling.
*/
function useRow(live: boolean) {
const row = useRef<HTMLDivElement>(null);
const [dist, setDist] = useState(0);
const [marks, setMarks] = useState<Mark[]>([]);
useEffect(() => {
const el = row.current;
if (!el || !live) {
setDist(0);
setMarks([]);
return;
}
const measure = () => {
const items = Array.from(el.querySelectorAll<HTMLElement>(':scope > [data-item]'));
const last = items[items.length - 1];
if (!last) return;
const padEnd = parseFloat(getComputedStyle(el).paddingRight) || 0;
const d = Math.max(0, Math.round(last.offsetLeft + last.offsetWidth + padEnd - el.clientWidth));
const line = el.clientWidth * LINE;
setDist(d);
// Items near the end never reach the line, so the marks are squeezed until the last one is reached at 92 %
const raw = items.map((it) => (it.offsetLeft - line) / Math.max(1, d));
const s = Math.min(1, 0.92 / Math.max(0.0001, raw[raw.length - 1]));
setMarks(items.map((it, i) => {
// already past the line at the start: always reached
if (raw[i] <= 0) return { a: -1, b: -0.999 };
const a = Math.min(1, raw[i] * s);
return { a: round(a, 4), b: round(Math.min(1, Math.max(a + 0.0001, a + (it.offsetWidth / Math.max(1, d)) * s)), 4) };
}));
};
measure();
const ro = new ResizeObserver(measure);
ro.observe(el);
return () => ro.disconnect();
}, [live]);
return { row, dist, marks };
}
/** 0 → 1 while --p runs through an item's mark. Finished without JavaScript. */
const through = (m?: Mark) => (m ? seg(m.a, m.b) : '1');
function Header({ eyebrow, title, description, action, aside }: Pick<SidewaysScrollProps, 'eyebrow' | 'title' | 'description' | 'action'> & { aside?: React.ReactNode }) {
return (
<div className="kit-container">
<div className="flex flex-col gap-6 md:flex-row md:items-end md:justify-between md:gap-10">
<Reveal className="max-w-[40rem]">
{eyebrow && <Eyebrow>{eyebrow}</Eyebrow>}
{title && <h2 className="text-balance text-h2 font-semibold text-foreground">{title}</h2>}
{description && <p className="mt-4 max-w-[34rem] text-pretty text-lead text-muted-foreground">{description}</p>}
</Reveal>
{(aside || action) && (
<div className="flex shrink-0 items-center gap-5">
{aside}
{action && (
<Button href={action.href} variant="secondary" icon="arrow-right">
{action.label}
</Button>
)}
</div>
)}
</div>
</div>
);
}
/* ---------- Items ---------- */
function Card({ it, i, mark }: { it: SidewaysItem; i: number; mark?: Mark }) {
return (
<article className="group relative flex h-full flex-col justify-between gap-10 overflow-hidden rounded-2xl border border-border bg-card p-6 shadow-[var(--highlight),var(--shadow-sm)] transition-[border-color,transform] duration-500 ease-[var(--ease-kit)] hover:-translate-y-1 hover:border-border-strong motion-reduce:transition-none sm:p-7">
<div className="flex items-start justify-between">
<span className="flex size-11 items-center justify-center rounded-xl border border-border bg-surface text-primary transition-colors duration-500 group-hover:bg-[color-mix(in_oklab,var(--primary)_12%,var(--surface))]">
<Icon name={it.icon ?? 'sparkle'} size={20} />
</span>
{/* The number lights up when the card reaches the reading line */}
<span className="relative text-[13px] font-medium tabular-nums text-subtle-foreground">
{pad(i + 1)}
<span aria-hidden data-p className="absolute inset-0 text-primary" style={{ opacity: through(mark) }}>
{pad(i + 1)}
</span>
</span>
</div>
<div>
{it.eyebrow && <p className="text-[12.5px] font-medium text-muted-foreground">{it.eyebrow}</p>}
<h3 className="mt-2 text-balance text-[1.5rem] font-semibold leading-tight tracking-[-0.015em] text-foreground">{it.title}</h3>
{/* Room for three lines in the row, so the titles of all cards stand on one line */}
{it.text && <p className="mt-3 text-pretty text-[15px] leading-relaxed text-muted-foreground md:min-h-[4.875em]">{it.text}</p>}
{it.href && (
<a href={it.href} className="mt-5 inline-flex min-h-11 items-center gap-1.5 text-[14px] font-medium text-foreground after:absolute after:inset-0 focus-visible:outline-none [&:focus-visible]:after:rounded-2xl [&:focus-visible]:after:outline-2 [&:focus-visible]:after:outline-solid [&:focus-visible]:after:-outline-offset-2 [&:focus-visible]:after:outline-primary">
{it.title}
<Icon name="arrow-right" size={15} className="transition-transform duration-300 group-hover:translate-x-0.5" />
</a>
)}
</div>
</article>
);
}
function Year({ it, i, n, mark, live }: { it: SidewaysItem; i: number; n: number; mark?: Mark; live: boolean }) {
return (
<div className="relative pl-8 md:pl-0 md:pt-10">
{/* The line: downwards on phones, across from 768 px. Each item draws its own piece. */}
<span aria-hidden className={cn('absolute left-[7px] top-2 w-px bg-border md:left-0 md:right-0 md:top-[7px] md:h-px md:w-auto', i === n - 1 ? 'bottom-0 md:right-auto md:w-12' : '-bottom-10 md:-right-0')} />
<span
aria-hidden
data-p
className={cn('absolute left-[7px] top-2 w-px origin-top bg-primary md:left-0 md:right-0 md:top-[7px] md:h-px md:w-auto md:origin-left', i === n - 1 ? 'bottom-0 md:right-auto md:w-12' : '-bottom-10 md:-right-0')}
style={{ transform: live ? `scaleX(${through(mark)})` : undefined }}
/>
<span aria-hidden className="absolute left-0 top-0 flex size-[15px] items-center justify-center rounded-full border border-border-strong bg-background">
<span data-p className="size-[7px] rounded-full bg-primary" style={{ opacity: live ? `calc(0.15 + ${through(mark)} * 0.85)` : undefined }} />
</span>
<p data-p className="text-[clamp(2.75rem,2rem+3vw,5.5rem)] font-semibold leading-none tracking-[-0.04em] tabular-nums text-foreground" style={{ opacity: live ? `calc(0.35 + ${through(mark)} * 0.65)` : undefined }}>
{it.year}
</p>
<h3 className="mt-4 text-balance text-[17px] font-semibold text-foreground">{it.title}</h3>
{/* Three lines of room in the row, so the pictures below stand on one line */}
{it.text && <p className="mt-2 max-w-[22rem] text-pretty text-[14.5px] leading-relaxed text-muted-foreground md:min-h-[4.875em]">{it.text}</p>}
{it.media && <div className="mt-5 h-36 max-w-[22rem] overflow-hidden rounded-lg bg-surface md:h-[min(10rem,20svh)] [&>*]:size-full">{it.media}</div>}
</div>
);
}
const GALLERY_W = ['md:w-[26rem]', 'md:w-[19rem]', 'md:w-[32rem]', 'md:w-[22rem]', 'md:w-[28rem]', 'md:w-[20rem]'];
function Picture({ it, i }: { it: SidewaysItem; i: number }) {
return (
<figure className="group">
<div className="relative h-[min(26rem,62vw)] overflow-hidden rounded-xl bg-surface md:h-[min(24rem,40svh)] [&>*]:size-full [&>*]:transition-transform [&>*]:duration-700 [&>*]:ease-[var(--ease-kit)] group-hover:[&>*]:scale-[1.04] motion-reduce:[&>*]:transition-none">{it.media}</div>
<figcaption className="mt-4 flex items-baseline justify-between gap-4">
<span className="min-w-0">
<span className="block truncate text-[15px] font-semibold text-foreground">{it.title}</span>
{it.text && <span className="block truncate text-[13.5px] text-muted-foreground">{it.text}</span>}
</span>
<span className="shrink-0 text-[12.5px] tabular-nums text-subtle-foreground">{it.eyebrow ?? pad(i + 1)}</span>
</figcaption>
</figure>
);
}
function Chapter({ it, i, t }: { it: SidewaysItem; i: number; t: (typeof TEXTS)['en'] }) {
return (
<article className="grid h-full grid-cols-1 overflow-hidden rounded-2xl border border-border bg-card shadow-[var(--highlight),var(--shadow-sm)] 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-10">
<p className="flex items-center gap-3 text-[13px] font-medium tabular-nums text-muted-foreground">
<span className="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 && <span className="text-foreground">{it.eyebrow}</span>}
</p>
<div>
<h3 className="text-balance text-[clamp(1.625rem,1.2rem+1.3vw,2.375rem)] font-semibold leading-[1.1] tracking-[-0.025em] text-foreground">{it.title}</h3>
{it.text && <p className="mt-4 max-w-[28rem] text-pretty text-[15.5px] leading-relaxed text-muted-foreground">{it.text}</p>}
{it.href && (
<a href={it.href} className="-mx-2 mt-6 inline-flex min-h-11 items-center gap-1.5 rounded-md px-2 text-[14px] font-medium text-foreground hover:underline hover:underline-offset-4 focus-visible:outline-2 focus-visible:outline-solid focus-visible:-outline-offset-2 focus-visible:outline-primary">
{t.more}
<span className="sr-only">, {it.title}</span>
<Icon name="arrow-right" size={15} />
</a>
)}
</div>
</div>
{it.media && <div className="relative min-h-[14rem] overflow-hidden border-t border-border bg-surface lg:border-l lg:border-t-0 [&>*]:absolute [&>*]:inset-0 [&>*]:size-full">{it.media}</div>}
</article>
);
}
function Product({ it }: { it: SidewaysItem }) {
return (
<article className="group relative">
<div className="relative aspect-[4/5] overflow-hidden rounded-xl bg-surface [&>*]:size-full [&>*]:transition-transform [&>*]:duration-700 [&>*]:ease-[var(--ease-kit)] group-hover:[&>*]:scale-[1.04] motion-reduce:[&>*]:transition-none">
{it.media}
{it.tag && <span className="absolute left-3 top-3 !size-auto rounded-full bg-background px-2.5 py-1 text-[11.5px] font-medium text-foreground shadow-[var(--shadow-sm)]">{it.tag}</span>}
</div>
<div className="mt-3 flex items-start justify-between gap-3">
<div className="min-w-0">
<h3 className="truncate text-[14.5px] font-medium text-foreground">
<a href={it.href ?? '#'} className="after:absolute after:inset-0 focus-visible:outline-none [&:focus-visible]:after:rounded-xl [&:focus-visible]:after:outline-2 [&:focus-visible]:after:outline-solid [&:focus-visible]:after:outline-offset-4 [&:focus-visible]:after:outline-primary">
{it.title}
</a>
</h3>
{it.eyebrow && <p className="truncate text-[13px] text-muted-foreground">{it.eyebrow}</p>}
</div>
{it.price && <p className="shrink-0 text-[14.5px] font-semibold tabular-nums text-foreground">{it.price}</p>}
</div>
</article>
);
}
/* ---------- Section ---------- */
const ITEM_W: Record<SidewaysScrollVariant, string> = {
cards: 'w-full md:h-[min(22rem,48svh)] md:w-[21rem] lg:w-[23rem]',
timeline: 'w-full md:w-[21rem] md:pr-10 lg:w-[24rem]',
gallery: 'w-[78%] shrink-0 snap-start sm:w-[22rem]',
chapters: 'w-full md:h-[min(30rem,60svh)] md:w-[min(82vw,44rem)] lg:w-[min(72vw,58rem)]',
products: 'w-[62%] shrink-0 snap-start sm:w-[15rem] md:w-[16rem]',
};
/** On phones: a list downwards (cards, timeline, chapters) or a swipe row (gallery, products) */
const PHONE: Record<SidewaysScrollVariant, 'list' | 'row'> = { cards: 'list', timeline: 'list', gallery: 'row', chapters: 'list', products: 'row' };
/** A section that scrolls sideways while the page scrolls down. */
export function SidewaysScroll({ variant = 'cards', eyebrow, title, description, items, action, backdrop, labels, className, ...rest }: SidewaysScrollProps) {
const t = useTexts(TEXTS, labels);
const live = useLive('(min-width: 768px)');
const { row, dist, marks } = useRow(live);
const running = live && dist > 0;
const n = items.length;
const [active, setActive] = useState(0);
// The counter walks evenly from the first to the last item
const back = useRef<HTMLParagraphElement>(null);
// The row and the word behind it move by a transform written straight onto them. A custom
// property on the row would restyle every picture in it on every frame.
const onProgress = useCallback(
(p: number) => {
setActive(Math.round(p * (n - 1)));
if (row.current) row.current.style.transform = `translate3d(${round(-p * dist, 1)}px, 0, 0)`;
if (back.current) back.current.style.transform = `translate3d(${round(-p * dist * 0.35, 1)}px, 0, 0)`;
},
[n, dist, row],
);
const track = useSceneProgress<HTMLDivElement>(running, { onProgress, self: false });
useEffect(() => {
if (running) return;
row.current?.style.removeProperty('transform');
back.current?.style.removeProperty('transform');
}, [running, row]);
const [fadeRef, fade] = useScrollFade<HTMLDivElement>();
const phone = PHONE[variant];
// Tab into an item that sits outside the window: scroll the page to the progress that shows it
const onFocus = (e: React.FocusEvent<HTMLDivElement>) => {
if (!running || !track.current) return;
const item = (e.target as HTMLElement).closest<HTMLElement>('[data-item]');
if (!item) return;
const i = Number(item.dataset.item);
const m = marks[i];
if (!m) return;
const top = track.current.getBoundingClientRect().top + window.scrollY;
const travel = track.current.offsetHeight - window.innerHeight;
window.scrollTo({ top: Math.round(top + Math.max(0, Math.min(1, m.a + 0.02)) * travel), behavior: 'instant' as ScrollBehavior });
};
const counter = (
<div className={cn('hidden items-center gap-4 md:flex', !running && 'md:hidden')} aria-hidden={!running}>
<span className="w-16 text-[12.5px] font-medium tabular-nums text-muted-foreground">
{pad(active + 1)} / {pad(n)}
</span>
<span className="relative h-0.5 w-32 overflow-hidden rounded-full bg-border lg:w-44">
<span data-p className="absolute inset-0 origin-left rounded-full bg-primary" style={{ transform: 'scaleX(var(--p, 1))' }} />
</span>
<span className="sr-only">
{active + 1} {t.of} {n}
</span>
</div>
);
const rowClass = cn(
'relative flex gap-5 md:gap-6',
variant === 'timeline' && 'gap-10 md:gap-0',
variant === 'gallery' && 'gap-4 md:gap-8',
// padding lines the first item up with the container, the last one ends on the same margin
'px-6 lg:px-[max(2rem,calc((100%-var(--container-kit))/2))]',
phone === 'list' ? 'max-md:flex-col' : 'kit-no-scrollbar snap-x snap-mandatory overflow-x-auto scroll-px-6 max-md:pb-2 md:snap-none',
running ? 'w-full overflow-visible will-change-transform' : 'md:kit-no-scrollbar md:overflow-x-auto',
!running && fade,
);
const content = (
<div
ref={(el) => {
row.current = el;
fadeRef.current = el;
}}
role={running ? undefined : 'region'}
aria-label={running ? undefined : t.region}
tabIndex={running || phone === 'list' ? undefined : 0}
onFocus={onFocus}
className={cn(rowClass, 'focus-visible:outline-2 focus-visible:outline-solid focus-visible:-outline-offset-2 focus-visible:outline-primary')}
>
{items.map((it, i) => (
<div key={i} data-item={i} className={cn('relative shrink-0 md:snap-start', running && 'will-change-transform', ITEM_W[variant], variant === 'gallery' && GALLERY_W[i % GALLERY_W.length])}>
{variant === 'cards' && <Card it={it} i={i} mark={running ? marks[i] : undefined} />}
{variant === 'timeline' && <Year it={it} i={i} n={n} mark={marks[i]} live={running} />}
{variant === 'gallery' && <Picture it={it} i={i} />}
{variant === 'chapters' && <Chapter it={it} i={i} t={t} />}
{variant === 'products' && <Product it={it} />}
</div>
))}
{variant === 'products' && action && (
<div data-item={n} className={cn('relative shrink-0 snap-start', ITEM_W.products)}>
<a href={action.href} className="group flex aspect-[4/5] flex-col items-center justify-center gap-4 rounded-xl border border-dashed border-border-strong text-center transition-colors duration-500 hover:border-primary hover:bg-[color-mix(in_oklab,var(--primary)_6%,transparent)] focus-visible:outline-2 focus-visible:outline-solid focus-visible:-outline-offset-2 focus-visible:outline-primary">
<span className="flex size-12 items-center justify-center rounded-full bg-primary text-primary-foreground transition-transform duration-500 group-hover:translate-x-1">
<Icon name="arrow-right" size={20} />
</span>
<span className="max-w-[10rem] text-balance text-[14.5px] font-medium text-foreground">{t.all}</span>
</a>
</div>
)}
</div>
);
return (
<section {...rest} className={cn('relative overflow-x-clip', !running && 'py-20 sm:py-24 lg:py-32', className)}>
<div ref={track} className="relative" style={running ? { height: `calc(100svh + ${dist}px)` } : undefined}>
<div className={cn(running && 'sticky top-0 flex h-svh flex-col justify-center gap-10 overflow-clip py-10 lg:gap-14')}>
<Header eyebrow={eyebrow} title={title} description={description} action={variant === 'products' ? undefined : action} aside={counter} />
<div className={cn('relative z-10', !running && 'mt-12 sm:mt-14')}>{content}</div>
{/* gallery: a large word behind the lower edge of the pictures, gliding at a third of their speed */}
{variant === 'gallery' && backdrop && (
<p
ref={back}
aria-hidden
className="pointer-events-none relative -mt-[0.8em] hidden select-none whitespace-nowrap pl-[max(2rem,calc((100%-var(--container-kit))/2))] text-[clamp(5rem,10vw,10rem)] font-semibold leading-none tracking-[-0.05em] text-[color-mix(in_oklab,var(--foreground)_9%,transparent)] md:block"
>
{backdrop}
</p>
)}
</div>
</div>
</section>
);
}Häufige Fragen
Wie installiere ich Seitlich scrollen?
Mit aiw add @ai-website/sideways-scroll. Der Befehl legt die Datei components/ui/sideways-scroll.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind button, icon, reveal, section, motion und scroll-scene. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Seitlich scrollen?
5 Varianten, Leistungskarten, Zeitleiste, Galerie mit Wort dahinter, Breite Kapitel und Produktreihe. Du wählst sie mit der Prop variant, zum Beispiel variant="timeline".
Kann ich die Texte von Seitlich scrollen übersetzen?
Ja. Seitlich scrollen bringt 4 eigene Texte auf Deutsch und Englisch mit, etwa „Seitlich scrollbare Reihe“, „von“ und „Mehr erfahren“. Die Sprache folgt dem KitProvider, einzelne Texte ersetzt du über die Prop labels.
Kann ein KI-Assistent Seitlich scrollen einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Seitlich scrollen mit aiw_components und installieren sie mit aiw_add @ai-website/sideways-scroll. Nenn im Prompt die Variante, etwa variant="cards", dann schreibt der Assistent deine Inhalte direkt in die Props.
Kann ich Seitlich scrollen ohne Code anpassen?
Ja, im Studio. Alle 5 Varianten lassen sich dort bearbeiten, Texte, Listen und Schalter. Das Studio schreibt den passenden SidewaysScroll-Code zum Kopieren.