Zum Inhalt springen
UI Kit
Accent

Tabs Section für Inhalte

Inhalte in Reitern mit Bild und Text, per Tastatur bedienbar. Reiterzeile, Liste mit Wechsel, Reiterkarten oder mittige Bühne.

4 VariantsEine von 3 Components in Content

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

Bild und TextNeu in 1.2

Eine Reiterzeile mit gleitender Linie über einem Panel mit Bild und Text nebeneinander.

Senkrecht mit WechselNeu in 1.2

Reiter mit Text links, die von selbst weiterschalten, mit füllender Linie und Pause, auf dem Telefon ein Akkordeon.

ReiterkartenNeu in 1.2

Reiter als Karten mit Icon und Zeile, darunter das Panel mit Bild, Punkten und Aktion.

BühneNeu in 1.2

Ein mittiger Umschalter über einem großen Bild, eine Bildkarte mit Zahlen liegt auf seinem unteren Rand.

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
Bringt mit
utilskiticonsectionbutton
npm-Pakete
Keine
Sprachen
Englisch und Deutsch, 3 eigene Texte
Studio
Im Code anpassen

Props

Die Schnittstelle ContentTabsProps aus components/ui/content-tabs.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.

  • variantoptional
    ContentTabsVariant

    image-textverticalcardsshowcase

  • eyebrowoptional
    React.ReactNode
  • titleoptional
    React.ReactNode
  • descriptionoptional
    React.ReactNode
  • tabsPflicht
    ContentTab[]
  • defaultIndexoptional
    number
  • intervaloptional
    number

    vertical: 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
tabsEnglisch TopicsDeutsch Themen
pauseEnglisch Pause the slideshowDeutsch Wechsel anhalten
playEnglisch Play the slideshowDeutsch 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
components/ui/content-tabs.tsx
'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.

Oft gesucht

Direkt zu

In die Zwischenablage kopiert