Tabs Section für Inhalte
Inhalte in Reitern mit Bild und Text, per Tastatur bedienbar. Reiterzeile, Liste mit Wechsel, Reiterkarten oder mittige Bühne.
Mehrere Geschichten an einem Ort
Inhalte in Reitern zeigen mehrere Features oder Anwendungsfälle auf dem Platz von einem, jeder mit eigenem Bild und Text. Sie sind per Tastatur bedienbar und funktionieren damit für alle, die die Seite besuchen.
„Bild und Text“ setzt eine Reiterzeile mit gleitender Linie über ein Panel mit Bild und Text nebeneinander. „Senkrecht mit Wechsel“ listet links Reiter mit Text, die mit füllender Linie und Pause von selbst weiterschalten, am Handy wird daraus ein Akkordeon. „Reiterkarten“ machen Reiter zu Karten mit Icon und Zeile, darunter Bild, Punkte und eine Aktion. Die „Bühne“ setzt einen mittigen Umschalter über ein großes Bild, eine Bildkarte mit Kennzahlen ragt über dessen Unterkante.
Drei bis fünf Reiter wirken am besten, benenne jeden nach seinem Nutzen. Der automatische Wechsel passt nah an den Einstieg, die Pause lässt Besuchern die Kontrolle. Die Bühne passt zu Produktbereichen mit je einem starken Bild.
Variants
Details
Was Inhalte in Reitern mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Content
- Bereich
- Content
- Variants
- 4
- Version
- 1.0.0 · seit 1.2.0
- Datei
- components/ui/content-tabs.tsx
- npm-Pakete
- Keine
- Sprachen
- Englisch und Deutsch, 3 eigene Texte
- Studio
- Im Code anpassen
- Mit KI bauen
- Per MCP in Claude, Cursor, Copilot und Codex
Props
Die Schnittstelle ContentTabsProps aus components/ui/content-tabs.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.
variantoptionalContentTabsVariantimage-textverticalcardsshowcase
eyebrowoptionalReact.ReactNodetitleoptionalReact.ReactNodedescriptionoptionalReact.ReactNodetabsPflichtContentTab[]defaultIndexoptionalnumberintervaloptionalnumbervertical: seconds per tab when it advances on its own, 0 turns it off
Eingebaute Texte
Diese Texte schreibt Inhalte in Reitern selbst. Die Sprache folgt dem KitProvider, über die Prop labels ersetzt du einzelne.
Alle 3 Texte anzeigen
| Schlüssel | Englisch | Deutsch |
|---|---|---|
| tabs | Englisch Topics | Deutsch Themen |
| pause | Englisch Pause the slideshow | Deutsch Wechsel anhalten |
| play | Englisch Play the slideshow | Deutsch Wechsel fortsetzen |
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- Inhalte in Reitern reagiert selbst auf die Tasten Pfeil hoch, Pfeil runter, Pfeil links, Pfeil rechts und Leertaste.
- Setzt aria-label, aria-orientation, aria-pressed und aria-selected, damit Screenreader Zweck und Zustand ansagen.
- Nutzt die Rollen presentation und tablist.
- Zeigt bei Bedienung mit der Tastatur einen sichtbaren Fokusring.
- Der Server rendert den Startzustand vollständig, JavaScript ergänzt nur die Interaktion.
Bewegung
- Bringt eigene Animationen mit, ruhig getaktet und ohne Wackeln.
- 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, 368 Zeilen
'use client';
import { useEffect, useId, useRef, useState } from 'react';
import { cn, prefersReducedMotion, useInView, useTexts } from '@/lib/kit';
import { Icon, type IconName } from '@/components/ui/icon';
import { Eyebrow, Section } from '@/components/ui/section';
import { Button } from '@/components/ui/button';
/*
* Content in tabs, each tab with a picture and text. Tabs follow the APG
* pattern: arrow keys move between them, Home and End jump to the ends,
* every panel is labelled by its tab. Without JavaScript all panels show
* one below the other.
*
* image-text a tab row above a panel with picture and text side by side
* vertical a list of tabs with their text on the left that advances
* on its own, with a pause; an accordion on phones
* cards tabs as cards with icon and line, the panel with points
* and an action below
* showcase a centered switch above a large picture with a caption
* card and figures
*/
export type ContentTabsVariant = 'image-text' | 'vertical' | 'cards' | 'showcase';
export interface ContentTab {
label: string;
icon?: IconName;
/** A short line under the label (vertical, cards) */
summary?: string;
title?: string;
text?: string;
points?: string[];
/** showcase: figures beside the caption */
stats?: { value: string; label: string }[];
action?: { label: string; href: string };
media?: React.ReactNode;
}
const TEXTS = {
en: { tabs: 'Topics', pause: 'Pause the slideshow', play: 'Play the slideshow' },
de: { tabs: 'Themen', pause: 'Wechsel anhalten', play: 'Wechsel fortsetzen' },
};
type Texts = (typeof TEXTS)['en'];
export interface ContentTabsProps {
variant?: ContentTabsVariant;
eyebrow?: React.ReactNode;
title?: React.ReactNode;
description?: React.ReactNode;
tabs: ContentTab[];
defaultIndex?: number;
/** vertical: seconds per tab when it advances on its own, 0 turns it off */
interval?: number;
labels?: Partial<Texts>;
className?: string;
}
const CSS = '@keyframes kitfx-ctabs-fill{from{transform:scaleY(0)}to{transform:scaleY(1)}}';
const focus = 'outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-ring';
const fillImg = '[&>*]:size-full';
/** Roving focus and selection for one tab list */
function useTabs(count: number, start = 0) {
const [index, setIndex] = useState(Math.min(Math.max(0, start), Math.max(0, count - 1)));
const [hydrated, setHydrated] = useState(false);
const refs = useRef<(HTMLButtonElement | null)[]>([]);
const id = useId();
useEffect(() => setHydrated(true), []);
const onKeyDown = (e: React.KeyboardEvent, i: number, vertical = false) => {
const next = { [vertical ? 'ArrowDown' : 'ArrowRight']: (i + 1) % count, [vertical ? 'ArrowUp' : 'ArrowLeft']: (i - 1 + count) % count, Home: 0, End: count - 1 }[e.key];
if (next === undefined) return;
e.preventDefault();
setIndex(next);
refs.current[next]?.focus();
};
const tab = (i: number, vertical = false) => ({
ref: (el: HTMLButtonElement | null) => void (refs.current[i] = el),
role: 'tab' as const,
id: `${id}-t${i}`,
'aria-selected': index === i,
'aria-controls': `${id}-p${i}`,
tabIndex: index === i ? 0 : -1,
onClick: () => setIndex(i),
onKeyDown: (e: React.KeyboardEvent) => onKeyDown(e, i, vertical),
});
const panel = (i: number) => ({ role: 'tabpanel' as const, id: `${id}-p${i}`, 'aria-labelledby': `${id}-t${i}`, tabIndex: 0, hidden: hydrated ? index !== i : false });
return { index, setIndex, tab, panel, hydrated };
}
function Head({ p, center = false }: { p: ContentTabsProps; center?: boolean }) {
if (!p.title && !p.eyebrow) return null;
return (
<div className={cn('mb-10 max-w-[44rem] sm:mb-12', center && 'mx-auto text-center')}>
{p.eyebrow && <Eyebrow>{p.eyebrow}</Eyebrow>}
{p.title && <h2 className="text-balance text-h2 font-semibold text-foreground">{p.title}</h2>}
{p.description && <p className={cn('mt-5 text-pretty text-lead text-muted-foreground', center && 'mx-auto max-w-[36rem]')}>{p.description}</p>}
</div>
);
}
function Points({ points, className }: { points?: string[]; className?: string }) {
if (!points?.length) return null;
return (
<ul className={cn('space-y-2.5', className)}>
{points.map((pt, i) => (
<li key={pt} className="flex gap-3 text-[15px] leading-relaxed text-foreground motion-safe:animate-[kit-rise_0.5s_var(--ease-kit)_both]" style={{ animationDelay: `${120 + i * 60}ms` }}>
<span className="mt-1 flex size-5 shrink-0 items-center justify-center rounded-full bg-[color-mix(in_oklab,var(--primary)_14%,transparent)] text-[color-mix(in_oklab,var(--primary)_85%,var(--foreground))]">
<Icon name="check" size={12} strokeWidth={2.4} />
</span>
{pt}
</li>
))}
</ul>
);
}
/* ------------------------------------------------------------------ */
function ImageText({ p, t }: { p: ContentTabsProps; t: Texts }) {
const { index, tab, panel } = useTabs(p.tabs.length, p.defaultIndex);
const list = useRef<HTMLDivElement>(null);
const [mark, setMark] = useState<{ left: number; width: number } | null>(null);
useEffect(() => {
const place = () => {
const on = list.current?.querySelector<HTMLElement>('[aria-selected="true"]');
if (!on || !list.current) return;
setMark({ left: on.offsetLeft, width: on.offsetWidth });
const l = list.current;
if (l.scrollWidth > l.clientWidth) l.scrollTo({ left: on.offsetLeft - l.clientWidth / 2 + on.offsetWidth / 2, behavior: prefersReducedMotion() ? 'auto' : 'smooth' });
};
place();
window.addEventListener('resize', place);
return () => window.removeEventListener('resize', place);
}, [index]);
return (
<>
<Head p={p} />
<div ref={list} role="tablist" aria-label={t.tabs} className="kit-no-scrollbar max-sm:kit-fade-end relative -mx-6 flex gap-1 overflow-x-auto border-b border-border px-6 sm:mx-0 sm:gap-2 sm:px-0">
{p.tabs.map((tb, i) => (
<button key={tb.label} type="button" {...tab(i)} className={cn('relative inline-flex min-h-12 shrink-0 cursor-pointer items-center gap-2 px-3 text-[14.5px] font-medium transition-colors sm:px-4', index === i ? 'text-foreground' : 'text-muted-foreground hover:text-foreground', focus, 'focus-visible:-outline-offset-2')}>
{tb.icon && <Icon name={tb.icon} size={16} />}
{tb.label}
</button>
))}
{mark && <span aria-hidden className="absolute bottom-0 h-0.5 rounded-full bg-primary transition-[left,width] duration-500 ease-[var(--ease-kit)] motion-reduce:transition-none" style={{ left: mark.left, width: mark.width }} />}
</div>
{p.tabs.map((tb, i) => (
<div key={tb.label} {...panel(i)} className={cn('pt-10 outline-none sm:pt-12', focus)}>
<div className="grid grid-cols-[minmax(0,1fr)] items-center gap-10 lg:grid-cols-2 lg:gap-16">
<div data-clip-ok className={cn('relative aspect-[4/3] md:aspect-[16/10] lg:aspect-[4/3] overflow-hidden rounded-2xl border border-border bg-muted shadow-[var(--shadow-md)] motion-safe:animate-[kit-rise_0.6s_var(--ease-kit)]', fillImg)}>{tb.media}</div>
<div className="motion-safe:animate-[kit-rise_0.6s_var(--ease-kit)_0.08s_both]">
{tb.title && <h3 className="text-balance text-[clamp(1.5rem,1.25rem+1vw,2rem)] font-semibold leading-tight tracking-[-0.025em] text-foreground">{tb.title}</h3>}
{tb.text && <p className="mt-4 max-w-[34rem] text-pretty text-[16.5px] leading-relaxed text-muted-foreground">{tb.text}</p>}
<Points points={tb.points} className="mt-6" />
{tb.action && (
<div className="mt-8">
<Button href={tb.action.href} variant="outline" icon="arrow-right" className="max-sm:w-full">
{tb.action.label}
</Button>
</div>
)}
</div>
</div>
</div>
))}
</>
);
}
function Vertical({ p, t }: { p: ContentTabsProps; t: Texts }) {
const { index, setIndex, tab, panel, hydrated } = useTabs(p.tabs.length, p.defaultIndex);
const seconds = p.interval ?? 7;
const [paused, setPaused] = useState(false);
const [hover, setHover] = useState(false);
const [ref, inView] = useInView<HTMLDivElement>();
const [reduce, setReduce] = useState(false);
useEffect(() => setReduce(prefersReducedMotion()), []);
const running = hydrated && seconds > 0 && !paused && !hover && inView && !reduce;
useEffect(() => {
if (!running) return;
const timer = window.setTimeout(() => setIndex((i) => (i + 1) % p.tabs.length), seconds * 1000);
return () => window.clearTimeout(timer);
}, [running, index, seconds, p.tabs.length, setIndex]);
return (
<div ref={ref} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} onFocus={() => setHover(true)} onBlur={() => setHover(false)}>
<style>{CSS}</style>
<Head p={p} />
<div className="grid grid-cols-[minmax(0,1fr)] gap-8 lg:grid-cols-[minmax(0,5fr)_minmax(0,7fr)] lg:gap-14">
<div>
<div role="tablist" aria-label={t.tabs} aria-orientation="vertical" className="divide-y divide-border border-y border-border">
{p.tabs.map((tb, i) => {
const on = index === i;
return (
<div key={tb.label} role="presentation">
<button type="button" {...tab(i, true)} className={cn('relative block w-full cursor-pointer py-5 pl-5 pr-2 text-left transition-colors', focus, 'focus-visible:-outline-offset-2')}>
<span aria-hidden className="absolute bottom-3 left-0 top-3 w-0.5 overflow-hidden rounded-full bg-track">
<span
key={`${index}-${running}`}
className={cn('block w-full origin-top bg-primary', on ? 'h-full' : 'h-0')}
style={on && running ? { animation: `kitfx-ctabs-fill ${seconds}s linear forwards` } : undefined}
/>
</span>
<span className="flex items-center gap-3">
{tb.icon && <Icon name={tb.icon} size={18} className={cn('shrink-0 transition-colors', on ? 'text-primary' : 'text-muted-foreground')} />}
<span className={cn('text-[17px] font-semibold tracking-[-0.015em] transition-colors', on ? 'text-foreground' : 'text-muted-foreground hover:text-foreground')}>{tb.label}</span>
</span>
<span className={cn('grid transition-[grid-template-rows,opacity] duration-500 ease-[var(--ease-kit)] motion-reduce:transition-none', on ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0')}>
<span className={cn('min-h-0 overflow-hidden', !on && 'invisible')}>
{tb.summary && <span className="mt-2 block max-w-[30rem] text-pretty text-[15px] leading-relaxed text-muted-foreground">{tb.summary}</span>}
</span>
</span>
</button>
{/* phones: the picture opens under its tab, like an accordion */}
{on && <div data-clip-ok className={cn('relative mb-5 aspect-[16/10] overflow-hidden rounded-xl border border-border bg-muted lg:hidden', fillImg)}>{tb.media}</div>}
</div>
);
})}
</div>
{seconds > 0 && (
<button type="button" onClick={() => setPaused((x) => !x)} aria-pressed={paused} className={cn('mt-4 inline-flex min-h-11 cursor-pointer items-center gap-2 rounded-lg px-3 text-[13px] font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground', focus)}>
<Icon name={paused ? 'play' : 'pause'} size={14} />
{paused ? t.play : t.pause}
</button>
)}
</div>
<div className="relative hidden lg:block">
{p.tabs.map((tb, i) => (
<div key={tb.label} {...panel(i)} className={cn('outline-none', focus)}>
<div data-clip-ok className={cn('relative aspect-[4/3] overflow-hidden rounded-2xl border border-border bg-muted shadow-[var(--shadow-lg)] motion-safe:animate-[kit-rise_0.6s_var(--ease-kit)]', fillImg)}>{tb.media}</div>
{(tb.title || tb.text) && (
<div className="mt-6 max-w-[36rem]">
{tb.title && <h3 className="text-[19px] font-semibold tracking-[-0.015em] text-foreground">{tb.title}</h3>}
{tb.text && <p className="mt-2 text-pretty text-[15.5px] leading-relaxed text-muted-foreground">{tb.text}</p>}
</div>
)}
</div>
))}
</div>
</div>
</div>
);
}
function Cards({ p, t }: { p: ContentTabsProps; t: Texts }) {
const { index, tab, panel } = useTabs(p.tabs.length, p.defaultIndex);
return (
<>
<Head p={p} center />
<div role="tablist" aria-label={t.tabs} className={cn('grid grid-cols-[minmax(0,1fr)] gap-3', p.tabs.length === 3 ? 'md:grid-cols-3' : p.tabs.length === 2 ? 'sm:grid-cols-2' : p.tabs.length === 4 ? 'sm:grid-cols-2 lg:grid-cols-4' : 'sm:grid-cols-2 lg:grid-cols-3')}>
{p.tabs.map((tb, i) => {
const on = index === i;
return (
<button
key={tb.label}
type="button"
{...tab(i)}
className={cn(
'group relative flex cursor-pointer items-start gap-4 overflow-hidden rounded-xl border p-5 text-left transition-[border-color,background-color,box-shadow,transform] duration-300 ease-[var(--ease-kit)]',
on ? 'border-[color-mix(in_oklab,var(--primary)_50%,var(--border))] bg-[color-mix(in_oklab,var(--primary)_7%,var(--card))] shadow-[var(--highlight),var(--shadow-md)]' : 'border-border bg-card shadow-[var(--highlight),var(--shadow-sm)] hover:-translate-y-0.5 hover:border-border-strong motion-reduce:hover:translate-y-0',
focus,
)}
>
{tb.icon && (
<span className={cn('flex size-10 shrink-0 items-center justify-center rounded-lg border transition-colors duration-300', on ? 'border-transparent bg-primary text-primary-foreground' : 'border-border bg-surface text-foreground')}>
<Icon name={tb.icon} size={19} />
</span>
)}
<span className="min-w-0">
<span className="block text-[15.5px] font-semibold text-foreground">{tb.label}</span>
{tb.summary && <span className="mt-1 block text-pretty text-[13.5px] leading-snug text-muted-foreground">{tb.summary}</span>}
</span>
<span aria-hidden className={cn('absolute inset-x-0 bottom-0 h-0.5 origin-left bg-primary transition-transform duration-500 ease-[var(--ease-kit)]', on ? 'scale-x-100' : 'scale-x-0')} />
</button>
);
})}
</div>
{p.tabs.map((tb, i) => (
<div key={tb.label} {...panel(i)} className={cn('mt-6 overflow-hidden rounded-2xl border border-border bg-card shadow-[var(--highlight),var(--shadow-md)] outline-none', focus)}>
<div className="grid grid-cols-[minmax(0,1fr)] lg:grid-cols-[minmax(0,6fr)_minmax(0,5fr)]">
<div className="order-2 p-6 sm:p-10 lg:order-1 lg:p-12">
{tb.title && <h3 className="text-balance text-[clamp(1.5rem,1.25rem+1vw,2rem)] font-semibold leading-tight tracking-[-0.025em] text-foreground motion-safe:animate-[kit-rise_0.5s_var(--ease-kit)]">{tb.title}</h3>}
{tb.text && <p className="mt-4 max-w-[34rem] text-pretty text-[16px] leading-relaxed text-muted-foreground">{tb.text}</p>}
<Points points={tb.points} className="mt-6" />
{tb.action && (
<div className="mt-8">
<Button href={tb.action.href} icon="arrow-right" className="max-sm:w-full">
{tb.action.label}
</Button>
</div>
)}
</div>
<div data-clip-ok className={cn('relative order-1 aspect-[16/10] overflow-hidden border-b border-border bg-muted lg:order-2 lg:aspect-auto lg:min-h-[22rem] lg:border-b-0 lg:border-l', fillImg)}>{tb.media}</div>
</div>
</div>
))}
</>
);
}
function Showcase({ p, t }: { p: ContentTabsProps; t: Texts }) {
const { index, tab, panel } = useTabs(p.tabs.length, p.defaultIndex);
return (
<>
<Head p={p} center />
<div className="flex justify-center">
<div role="tablist" aria-label={t.tabs} className="kit-no-scrollbar max-sm:kit-fade-end -mx-6 flex max-w-[calc(100%+3rem)] gap-1 overflow-x-auto px-6 sm:mx-0 sm:max-w-full sm:rounded-xl sm:border sm:border-border sm:bg-surface sm:p-1">
{p.tabs.map((tb, i) => (
<button
key={tb.label}
type="button"
{...tab(i)}
className={cn('inline-flex min-h-11 shrink-0 cursor-pointer items-center gap-2 rounded-lg border px-4 text-[14px] font-medium transition-[background-color,color,box-shadow,border-color] duration-300 sm:border-transparent', index === i ? 'border-border bg-background text-foreground shadow-[var(--shadow-sm)]' : 'border-border text-muted-foreground hover:text-foreground', focus)}
>
{tb.icon && <Icon name={tb.icon} size={16} />}
{tb.label}
</button>
))}
</div>
</div>
{p.tabs.map((tb, i) => (
<div key={tb.label} {...panel(i)} className={cn('mx-auto mt-8 max-w-[68rem] outline-none', focus)}>
<div className="relative">
<div aria-hidden className="absolute -inset-x-4 -inset-y-6 -z-10 lg:-inset-x-8 rounded-[3rem] bg-[radial-gradient(closest-side,var(--glow),transparent)] blur-2xl" />
<div data-clip-ok className={cn('relative aspect-[4/3] overflow-hidden rounded-2xl border border-border bg-muted shadow-[var(--shadow-lg)] motion-safe:animate-[kit-rise_0.6s_var(--ease-kit)] sm:aspect-[16/9]', fillImg)}>{tb.media}</div>
{(tb.title || tb.text || tb.stats) && (
<div className="relative mx-3 -mt-10 rounded-xl border border-border bg-background/90 p-5 shadow-[var(--shadow-lg)] backdrop-blur-md motion-safe:animate-[kit-rise_0.6s_var(--ease-kit)_0.12s_both] sm:mx-8 sm:-mt-16 sm:p-6 lg:mx-auto lg:max-w-[52rem]">
<div className="grid grid-cols-[minmax(0,1fr)] gap-5 md:grid-cols-[minmax(0,1fr)_auto] md:items-center md:gap-10">
<div>
{tb.title && <h3 className="text-balance text-[19px] font-semibold tracking-[-0.015em] text-foreground">{tb.title}</h3>}
{tb.text && <p className="mt-1.5 text-pretty text-[15px] leading-relaxed text-muted-foreground">{tb.text}</p>}
</div>
{tb.stats?.length ? (
<dl className="flex gap-8 border-t border-border pt-4 md:border-l md:border-t-0 md:pl-8 md:pt-0">
{tb.stats.map((s) => (
<div key={s.label}>
<dt className="text-[12.5px] text-muted-foreground">{s.label}</dt>
<dd className="text-[22px] font-semibold tabular-nums tracking-[-0.02em] text-foreground">{s.value}</dd>
</div>
))}
</dl>
) : null}
</div>
</div>
)}
</div>
</div>
))}
</>
);
}
/**
* Content in tabs with picture and text: a tab row, a vertical list that
* advances, tab cards with a panel or a centered switch above a showcase.
* <ContentTabs variant="image-text" tabs={[{ label, title, text, points, media }]} />
*/
export function ContentTabs(props: ContentTabsProps) {
const { variant = 'image-text', className, labels } = props;
const t = useTexts(TEXTS, labels);
return (
<Section className={className}>
{variant === 'vertical' ? <Vertical p={props} t={t} /> : variant === 'cards' ? <Cards p={props} t={t} /> : variant === 'showcase' ? <Showcase p={props} t={t} /> : <ImageText p={props} t={t} />}
</Section>
);
}Häufige Fragen
Wie installiere ich Inhalte in Reitern?
Mit aiw add @ai-website/content-tabs. Der Befehl legt die Datei components/ui/content-tabs.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind icon, section und button. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Inhalte in Reitern?
4 Varianten, Bild und Text, Senkrecht mit Wechsel, Reiterkarten und Bühne. Du wählst sie mit der Prop variant, zum Beispiel variant="vertical".
Kann ich die Texte von Inhalte in Reitern übersetzen?
Ja. Inhalte in Reitern bringt 3 eigene Texte auf Deutsch und Englisch mit, etwa „Themen“, „Wechsel anhalten“ und „Wechsel fortsetzen“. Die Sprache folgt dem KitProvider, einzelne Texte ersetzt du über die Prop labels.
Kann ein KI-Assistent Inhalte in Reitern einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Inhalte in Reitern mit aiw_components und installieren sie mit aiw_add @ai-website/content-tabs. Nenn im Prompt die Variante, etwa variant="image-text", dann schreibt der Assistent deine Inhalte direkt in die Props.