Ankündigungsleiste
Die schmale Zeile über einer Seite, die sagt, was neu ist: wechselnde Meldungen, eine leuchtende Kapsel über der Überschrift, eine Launch-Woche, ein geplanter Hinweis mit Details und ein Sprachvorschlag.
Eine Zeile, die sagt, was neu ist
Eine Ankündigungsleiste ist die schmale Zeile über einer Seite, die allen sagt, was neu ist, ohne Popup und ohne den Hero zu verdrängen. Die fünf Fassungen hier decken Neuigkeiten, einen Launch, ein geplantes Zeitfenster und ein Sprachangebot ab.
„Wechselnde Meldungen“ lässt mehrere kurze Meldungen auf einer dunklen Leiste wechseln, eine Haarlinie zeigt die verbleibende Zeit, Pfeile und Pause stehen vorn, unter dem Zeiger hält die Leiste an. „Kapsel über der Überschrift“ setzt Etikett und Pfeil über die Hero-Überschrift, ab und zu läuft ein Licht über den Rand. Die „Launch-Woche“ zeigt fünf Tage mit einer Enthüllung je Tag, enthüllte Tage führen zur Meldung, heute leuchtet und eine Linie füllt sich mit der Woche. Der „Geplante Hinweis“ ist eine ruhige, getönte Leiste für ein geplantes Zeitfenster mit Datum und Uhrzeit im Format der Sprache, Details öffnen sich direkt und sagen, was weiter funktioniert. Der „Sprachvorschlag“ bietet die Seite in einer anderen Sprache an, mit echtem Formular zum Wählen, Wechseln oder Bleiben.
Sag je Meldung eine Sache und verlinke dorthin, wo sie erklärt wird. Halte die Leiste ruhig, Bewegung gehört der Seite darunter. Zeitgebundene Hinweise sollten von selbst enden, ein veralteter Hinweis kostet mehr Vertrauen als keiner.
Variants
Details
Was Ankündigungsleiste mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Banners
- Bereich
- Marketing
- Variants
- 5
- Version
- 1.0.0 · seit 1.2.0
- Datei
- components/ui/announcement-bar.tsx
- Bringt mit
- utilskiticon
- npm-Pakete
- Keine
- Sprachen
- Englisch und Deutsch, 17 eigene Texte
- Studio
- Im Code anpassen
- In Sammlungen
- SaaS-Landingpage
- Mit KI bauen
- Per MCP in Claude, Cursor, Copilot und Codex
Props
Die Schnittstelle AnnouncementBarProps aus components/ui/announcement-bar.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.
variantoptionalAnnouncementBarVariantrotatingpilllaunch-weeknoticelocale-suggest
messagesoptionalAnnouncementMessage[]rotating: the messages that take turns
intervaloptionalnumberrotating: seconds per message
tagoptionalstringpill, notice: tag in front
textoptionalstringpill, notice, launch-week, locale-suggest: the line itself
linkoptionalAnnouncementLinkpill, notice, launch-week: where the line leads
iconoptionalIconNamedaysoptionalAnnouncementDay[]launch-week: one entry per day
toneoptional'info' | 'maintenance' | 'success'notice: tone of the notice
windowoptional{ date: string; from: string; to: string }notice: the window, times as HH:MM, written in the format of the language
detailsoptionalstring[]notice: what happens in the window, shown when opened
languagesoptional{ label: string; href: string }[]locale-suggest: the languages to offer, the first one is suggested
dismissibleoptionalbooleanShows a close button. The bar stays closed for this visit.
centeroptionalbooleanCenters the pill and the bars on the page width (default true)
Eingebaute Texte
Diese Texte schreibt Ankündigungsleiste selbst. Die Sprache folgt dem KitProvider, über die Prop labels ersetzt du einzelne.
Alle 17 Texte anzeigen
| Schlüssel | Englisch | Deutsch |
|---|---|---|
| close | Englisch Close announcement | Deutsch Hinweis schließen |
| prev | Englisch Previous message | Deutsch Vorherige Meldung |
| next | Englisch Next message | Deutsch Nächste Meldung |
| pause | Englisch Pause messages | Deutsch Meldungen anhalten |
| play | Englisch Play messages | Deutsch Meldungen abspielen |
| of | Englisch {n} of {t} | Deutsch {n} von {t} |
| today | Englisch Today | Deutsch Heute |
| done | Englisch Revealed | Deutsch Enthüllt |
| soon | Englisch Soon | Deutsch Bald |
| details | Englisch Show details | Deutsch Details zeigen |
| hide | Englisch Hide details | Deutsch Details ausblenden |
| window | Englisch {date}, {from} to {to} | Deutsch {date}, {from} bis {to} Uhr |
| pick | Englisch Language | Deutsch Sprache |
| stay | Englisch Stay here | Deutsch Hier bleiben |
| go | Englisch Switch | Deutsch Wechseln |
| week | Englisch Launch week | Deutsch Launch-Woche |
| messages | Englisch Announcements | Deutsch Ankündigungen |
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- Setzt aria-controls, aria-current, aria-expanded, aria-label, aria-live, aria-pressed und aria-roledescription, damit Screenreader Zweck und Zustand ansagen.
- Nutzt die Rollen region und status.
- Meldet Änderungen über eine Live-Region, ohne den Fokus zu verschieben.
- Jedes Feld hat ein Label, sichtbar oder für Screenreader.
- 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).
- Bringt eigene Animationen mit, ruhig getaktet und ohne Wackeln.
- Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig. 8 Stellen im Code schalten Bewegung dann ab.
Quellcode
Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.
Datei anzeigen, 514 Zeilen
'use client';
import { useEffect, useId, useState } from 'react';
import { cn, formatTime, prefersReducedMotion, useLocale, useTexts } from '@/lib/kit';
import { Icon, type IconName } from '@/components/ui/icon';
/*
* The thin line above a page that says what is new. Five layouts, each for a
* different reason to speak up:
*
* rotating several short messages in turn, with arrows, pause and close
* pill a lit capsule above a headline, the classic "new" pointer
* launch-week a week of daily reveals, today lit, the rest waiting
* notice a planned window (maintenance, closed days) with details to open
* locale-suggest offers the page in the visitor's language, remembered once chosen
*/
export type AnnouncementBarVariant = 'rotating' | 'pill' | 'launch-week' | 'notice' | 'locale-suggest';
export interface AnnouncementLink {
label: string;
href: string;
}
export interface AnnouncementMessage {
text: string;
/** Short tag in front, e.g. New */
tag?: string;
icon?: IconName;
link?: AnnouncementLink;
}
export interface AnnouncementDay {
/** Short day label, e.g. Mon */
day: string;
title: string;
href?: string;
status: 'done' | 'today' | 'next';
}
export interface AnnouncementBarProps {
variant?: AnnouncementBarVariant;
/** rotating: the messages that take turns */
messages?: AnnouncementMessage[];
/** rotating: seconds per message */
interval?: number;
/** pill, notice: tag in front */
tag?: string;
/** pill, notice, launch-week, locale-suggest: the line itself */
text?: string;
/** pill, notice, launch-week: where the line leads */
link?: AnnouncementLink;
icon?: IconName;
/** launch-week: one entry per day */
days?: AnnouncementDay[];
/** notice: tone of the notice */
tone?: 'info' | 'maintenance' | 'success';
/** notice: the window, times as HH:MM, written in the format of the language */
window?: { date: string; from: string; to: string };
/** notice: what happens in the window, shown when opened */
details?: string[];
/** locale-suggest: the languages to offer, the first one is suggested */
languages?: { label: string; href: string }[];
/** Shows a close button. The bar stays closed for this visit. */
dismissible?: boolean;
/** Centers the pill and the bars on the page width (default true) */
center?: boolean;
labels?: Partial<Texts>;
className?: string;
}
const TEXTS = {
en: {
close: 'Close announcement',
prev: 'Previous message',
next: 'Next message',
pause: 'Pause messages',
play: 'Play messages',
of: '{n} of {t}',
today: 'Today',
done: 'Revealed',
soon: 'Soon',
details: 'Show details',
hide: 'Hide details',
window: '{date}, {from} to {to}',
pick: 'Language',
stay: 'Stay here',
go: 'Switch',
week: 'Launch week',
messages: 'Announcements',
},
de: {
close: 'Hinweis schließen',
prev: 'Vorherige Meldung',
next: 'Nächste Meldung',
pause: 'Meldungen anhalten',
play: 'Meldungen abspielen',
of: '{n} von {t}',
today: 'Heute',
done: 'Enthüllt',
soon: 'Bald',
details: 'Details zeigen',
hide: 'Details ausblenden',
window: '{date}, {from} bis {to} Uhr',
pick: 'Sprache',
stay: 'Hier bleiben',
go: 'Wechseln',
week: 'Launch-Woche',
messages: 'Ankündigungen',
},
};
type Texts = (typeof TEXTS)['en'];
type ViewProps = AnnouncementBarProps & { t: Texts; onClose: () => void };
const fill = (s: string, v: Record<string, string | number>) => s.replace(/\{(\w+)\}/g, (_, k) => String(v[k] ?? ''));
function Close({ onClose, t, className }: { onClose: () => void; t: Texts; className?: string }) {
return (
<button
type="button"
onClick={onClose}
aria-label={t.close}
className={cn(
'relative grid size-8 shrink-0 cursor-pointer place-items-center rounded-md text-current opacity-70 transition-[opacity,background-color] hover:bg-[color-mix(in_oklab,currentColor_10%,transparent)] hover:opacity-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-current after:absolute after:-inset-1.5',
className,
)}
>
<Icon name="x" size={16} />
</button>
);
}
function Arrow({ link, className }: { link: AnnouncementLink; className?: string }) {
return (
<a href={link.href} className={cn('group/link inline-flex min-h-11 shrink-0 cursor-pointer items-center gap-1 font-medium underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-current sm:min-h-8', className)}>
{link.label}
<Icon name="arrow-right" size={14} className="transition-transform duration-300 ease-[var(--ease-kit)] group-hover/link:translate-x-0.5 motion-reduce:transition-none" />
</a>
);
}
/* ------------------------------------------------------------------ */
/* rotating */
/* ------------------------------------------------------------------ */
function Rotating(p: ViewProps) {
const { t } = p;
const items = p.messages?.length ? p.messages : [{ text: p.text ?? '', link: p.link }];
const [i, setI] = useState(0);
const [paused, setPaused] = useState(false);
const [hold, setHold] = useState(false);
const [moved, setMoved] = useState(false);
const n = items.length;
const ms = Math.max(3, p.interval ?? 6) * 1000;
useEffect(() => {
if (n < 2 || paused || hold || prefersReducedMotion()) return;
const id = window.setTimeout(() => {
setI((x) => (x + 1) % n);
setMoved(true);
}, ms);
return () => window.clearTimeout(id);
}, [i, n, paused, hold, ms]);
const go = (d: number) => {
setI((x) => (x + d + n) % n);
setMoved(true);
};
const m = items[Math.min(i, n - 1)];
const running = n > 1 && !paused && !hold;
return (
<div
role="region"
aria-roledescription="carousel"
aria-label={t.messages}
onPointerEnter={() => setHold(true)}
onPointerLeave={() => setHold(false)}
onFocus={() => setHold(true)}
onBlur={(e) => !e.currentTarget.contains(e.relatedTarget as Node) && setHold(false)}
className={cn('dark relative overflow-hidden bg-bezel text-foreground', p.className)}
>
{/* the time left for this message, a hairline that fills along the bottom edge */}
{n > 1 && (
<span aria-hidden className="absolute inset-x-0 bottom-0 h-px bg-border">
<span
key={`${i}-${running}`}
className={cn('block h-full origin-left bg-primary motion-reduce:hidden', running ? 'animate-kit-progress' : 'scale-x-0')}
style={{ '--kit-duration': `${ms}ms` } as React.CSSProperties}
/>
</span>
)}
<div className="kit-container flex min-h-11 items-center gap-2 py-1.5 sm:gap-3">
{n > 1 && (
<div className="flex shrink-0 items-center max-sm:hidden">
<button type="button" onClick={() => go(-1)} aria-label={t.prev} className="relative grid size-8 cursor-pointer place-items-center rounded-md text-muted-foreground transition-colors hover:bg-[color-mix(in_oklab,var(--foreground)_8%,transparent)] hover:text-foreground focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-primary">
<Icon name="chevron-left" size={16} />
</button>
<button type="button" onClick={() => setPaused((x) => !x)} aria-label={paused ? t.play : t.pause} aria-pressed={paused} className="relative grid size-8 cursor-pointer place-items-center rounded-md text-muted-foreground transition-colors hover:bg-[color-mix(in_oklab,var(--foreground)_8%,transparent)] hover:text-foreground focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-primary">
<Icon name={paused ? 'play' : 'pause'} size={14} />
</button>
<button type="button" onClick={() => go(1)} aria-label={t.next} className="relative grid size-8 cursor-pointer place-items-center rounded-md text-muted-foreground transition-colors hover:bg-[color-mix(in_oklab,var(--foreground)_8%,transparent)] hover:text-foreground focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-primary">
<Icon name="chevron-right" size={16} />
</button>
</div>
)}
<div aria-live={running ? 'off' : 'polite'} className="relative flex min-w-0 flex-1 items-center justify-center">
<p key={i} className={cn('flex min-w-0 flex-wrap items-center justify-center gap-x-3 gap-y-0.5 text-center text-[13.5px] leading-snug', moved && 'animate-[ab-in_.5s_var(--ease-kit)] motion-reduce:animate-none')}>
<span className="sr-only">{fill(t.of, { n: i + 1, t: n })}. </span>
{m.tag && <span className="inline-flex h-5 items-center rounded-full bg-primary px-2 text-[11px] font-semibold text-primary-foreground">{m.tag}</span>}
{m.icon && <Icon name={m.icon} size={15} className="text-primary" />}
<span className="text-pretty text-foreground">{m.text}</span>
{m.link && <Arrow link={m.link} className="text-[13.5px] text-muted-foreground hover:text-foreground" />}
</p>
</div>
{n > 1 && (
// phones: pause and next, so the turning messages can always be stopped
<div className="-mr-1 flex shrink-0 items-center sm:hidden">
<button type="button" onClick={() => setPaused((x) => !x)} aria-label={paused ? t.play : t.pause} aria-pressed={paused} className="relative grid size-11 cursor-pointer place-items-center rounded-md text-muted-foreground focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-solid focus-visible:outline-primary">
<Icon name={paused ? 'play' : 'pause'} size={14} />
</button>
<button type="button" onClick={() => go(1)} aria-label={t.next} className="relative grid size-11 cursor-pointer place-items-center rounded-md text-muted-foreground focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-solid focus-visible:outline-primary">
<Icon name="chevron-right" size={16} />
</button>
</div>
)}
{n > 1 && (
<span aria-hidden className="shrink-0 text-[12px] tabular-nums text-subtle-foreground max-sm:hidden">
{i + 1}/{n}
</span>
)}
{p.dismissible && <Close onClose={p.onClose} t={t} className="max-sm:size-11" />}
</div>
<style>{'@keyframes ab-in{from{opacity:0;transform:translateY(60%);filter:blur(3px)}}'}</style>
</div>
);
}
/* ------------------------------------------------------------------ */
/* pill */
/* ------------------------------------------------------------------ */
function Pill(p: ViewProps) {
const center = p.center !== false;
const inner = (
<>
{p.tag && (
<span className="relative inline-flex h-6 shrink-0 items-center gap-1.5 rounded-full bg-primary px-2.5 text-[12px] font-semibold text-primary-foreground shadow-[0_0_14px_var(--glow)]">
{p.icon && <Icon name={p.icon} size={13} />}
{p.tag}
</span>
)}
<span className="min-w-0 text-pretty py-1 text-left text-[13.5px] leading-snug text-foreground sm:truncate sm:py-0">{p.text}</span>
{p.link && (
<span className="ml-auto flex shrink-0 items-center gap-1 pr-1 text-[13px] font-medium text-muted-foreground transition-colors group-hover/pill:text-foreground">
<span className="max-sm:sr-only">{p.link.label}</span>
<Icon name="arrow-right" size={14} className="transition-transform duration-300 ease-[var(--ease-kit)] group-hover/pill:translate-x-0.5 motion-reduce:transition-none" />
</span>
)}
</>
);
const cls =
'group/pill kit-rise relative isolate inline-flex min-h-11 max-w-full items-center gap-3 rounded-[1.4rem] sm:rounded-full border border-border bg-[color-mix(in_oklab,var(--card)_80%,transparent)] p-1 pr-3 shadow-[var(--highlight),var(--shadow-sm)] backdrop-blur transition-[border-color,box-shadow,transform] duration-300 ease-[var(--ease-kit)] hover:-translate-y-px hover:border-[color-mix(in_oklab,var(--primary)_40%,var(--border))] hover:shadow-[var(--highlight),0_0_0_4px_var(--glow),var(--shadow-md)] motion-reduce:hover:translate-y-0 sm:min-h-9';
return (
<div className={cn('flex', center && 'justify-center', p.className)}>
<span className="relative inline-flex max-w-full">
{/* a light that circles the rim once in a while */}
<span aria-hidden className="pointer-events-none absolute -inset-px overflow-hidden rounded-full motion-reduce:hidden">
<span className="absolute inset-y-0 -left-1/3 w-1/3 animate-[ab-sweep_5.5s_var(--ease-kit)_1.2s_infinite] bg-[linear-gradient(90deg,transparent,color-mix(in_oklab,var(--primary)_55%,transparent),transparent)] opacity-60" />
</span>
{p.link ? (
<a href={p.link.href} className={cn(cls, 'cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-primary')}>
{inner}
</a>
) : (
<p className={cls}>{inner}</p>
)}
</span>
<style>{'@keyframes ab-sweep{0%{transform:translateX(0)}35%,100%{transform:translateX(420%)}}'}</style>
</div>
);
}
/* ------------------------------------------------------------------ */
/* launch-week */
/* ------------------------------------------------------------------ */
function LaunchWeek(p: ViewProps) {
const { t } = p;
const days = p.days ?? [];
const today = Math.max(0, days.findIndex((d) => d.status === 'today'));
const done = days.filter((d) => d.status === 'done').length + (days.some((d) => d.status === 'today') ? 1 : 0);
const pct = days.length ? Math.round((done / days.length) * 100) : 0;
const current = days[today];
return (
<div className={cn('relative border-b border-border bg-surface', p.className)}>
<div className="kit-container flex min-h-12 flex-wrap items-center gap-x-5 gap-y-2 py-2.5 lg:flex-nowrap">
<p className={cn('flex min-w-0 flex-1 items-center gap-3 text-[13.5px] leading-snug', p.dismissible && 'max-sm:pr-9')}>
<span className="relative flex size-2 shrink-0" aria-hidden>
<span className="absolute inset-0 animate-ping rounded-full bg-primary opacity-60 motion-reduce:animate-none" />
<span className="relative size-2 rounded-full bg-primary" />
</span>
<span className="min-w-0 text-pretty">
<span className="font-semibold text-foreground">{p.tag ?? t.week}</span>
<span className="text-muted-foreground"> · </span>
{current ? (
<>
<span className="font-medium text-primary">{t.today}</span> <span className="text-muted-foreground">{current.title}</span>
</>
) : (
<span className="text-muted-foreground">{p.text}</span>
)}
</span>
</p>
<ol aria-label={p.tag ?? t.week} className="flex w-full items-center gap-1 sm:w-auto">
{days.map((d, i) => {
const state = d.status;
const content = (
<>
<span className="text-[11px] font-medium uppercase tracking-[0.08em]">{d.day}</span>
<span className="sr-only">
{', '}
{d.title}, {state === 'done' ? t.done : state === 'today' ? t.today : t.soon}
</span>
{state === 'done' && <Icon name="check" size={12} strokeWidth={2.2} className="text-primary" />}
</>
);
const cls = cn(
'relative flex h-11 min-w-0 flex-1 items-center justify-center gap-1 rounded-md px-2 transition-[background-color,color,box-shadow] duration-300 sm:h-8 sm:flex-none sm:px-2.5',
state === 'today' && 'bg-primary text-primary-foreground shadow-[0_0_0_3px_var(--glow)]',
state === 'done' && 'bg-card text-foreground ring-1 ring-inset ring-border hover:ring-border-strong',
state === 'next' && 'text-subtle-foreground',
);
return (
<li key={i} className="flex min-w-0 flex-1 sm:flex-none" style={{ '--kit-delay': `${i * 60}ms` } as React.CSSProperties}>
{d.href && state !== 'next' ? (
<a href={d.href} aria-current={state === 'today' ? 'date' : undefined} title={d.title} className={cn(cls, 'kit-rise cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-primary')}>
{content}
</a>
) : (
<span title={d.title} className={cn(cls, 'kit-rise')}>
{content}
</span>
)}
</li>
);
})}
</ol>
{p.link && <Arrow link={p.link} className="text-[13.5px] text-foreground max-lg:hidden" />}
{p.dismissible && <Close onClose={p.onClose} t={t} className="text-muted-foreground max-sm:absolute max-sm:right-2 max-sm:top-1.5 max-sm:size-9" />}
</div>
<span aria-hidden className="absolute inset-x-0 -bottom-px h-0.5">
<span className="block h-full origin-left rounded-r-full bg-primary transition-transform duration-1000 ease-[var(--ease-kit)]" style={{ transform: `scaleX(${pct / 100})` }} />
</span>
</div>
);
}
/* ------------------------------------------------------------------ */
/* notice */
/* ------------------------------------------------------------------ */
const TONE: Record<NonNullable<AnnouncementBarProps['tone']>, { icon: IconName; cls: string; dot: string }> = {
info: { icon: 'info', cls: 'bg-[color-mix(in_oklab,var(--primary)_9%,var(--background))] border-[color-mix(in_oklab,var(--primary)_24%,var(--border))]', dot: 'text-primary' },
maintenance: { icon: 'wrench', cls: 'bg-[color-mix(in_oklab,var(--warning)_14%,var(--background))] border-[color-mix(in_oklab,var(--warning)_40%,var(--border))]', dot: 'text-warning-foreground' },
success: { icon: 'check-circle', cls: 'bg-[color-mix(in_oklab,var(--success)_10%,var(--background))] border-[color-mix(in_oklab,var(--success)_30%,var(--border))]', dot: 'text-success-foreground' },
};
function Notice(p: ViewProps) {
const { t } = p;
const locale = useLocale();
const id = useId();
const [open, setOpen] = useState(false);
const tone = TONE[p.tone ?? 'info'];
const when = p.window ? fill(t.window, { date: p.window.date, from: formatTime(p.window.from, locale), to: formatTime(p.window.to, locale) }) : null;
const hasDetails = Boolean(p.details?.length);
return (
<div role="status" className={cn('border-b', tone.cls, p.className)}>
<div className="kit-container flex items-start gap-3 py-2.5 sm:items-center">
<Icon name={p.icon ?? tone.icon} size={17} className={cn('mt-0.5 shrink-0 sm:mt-0', tone.dot)} />
<div className="flex min-w-0 flex-1 flex-col gap-x-4 gap-y-1 text-[13.5px] leading-snug sm:flex-row sm:flex-wrap sm:items-center">
<p className="min-w-0 text-pretty text-foreground">
{p.tag && <span className="font-semibold">{p.tag} · </span>}
{p.text}
</p>
{when && (
<p className="flex items-center gap-1.5 text-muted-foreground">
<Icon name="clock" size={14} />
<span className="tabular-nums">{when}</span>
</p>
)}
</div>
<div className="flex shrink-0 items-center gap-1">
{hasDetails && (
<button
type="button"
aria-expanded={open}
aria-controls={`${id}-d`}
onClick={() => setOpen((x) => !x)}
className="inline-flex min-h-11 min-w-11 cursor-pointer items-center justify-center gap-1 rounded-md px-2 text-[13px] font-medium text-foreground hover:bg-[color-mix(in_oklab,var(--foreground)_6%,transparent)] focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-primary sm:min-h-8"
>
<span className="max-sm:sr-only">{open ? t.hide : t.details}</span>
<Icon name="chevron-down" size={15} className={cn('transition-transform duration-300 ease-[var(--ease-kit)]', open && 'rotate-180')} />
</button>
)}
{p.dismissible && <Close onClose={p.onClose} t={t} className="max-sm:size-11" />}
</div>
</div>
{hasDetails && (
<div id={`${id}-d`} className={cn('grid transition-[grid-template-rows] duration-500 ease-[var(--ease-kit)] motion-reduce:transition-none', open ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]')}>
<div className={cn('overflow-hidden', !open && 'invisible')}>
<div className="kit-container pb-4">
<ul className="grid gap-2 border-t border-[color-mix(in_oklab,var(--foreground)_10%,transparent)] pt-3 pl-8 text-[13.5px] text-muted-foreground sm:grid-cols-2 lg:grid-cols-3">
{p.details!.map((d, i) => (
<li key={i} className="flex gap-2">
<Icon name="check" size={14} className={cn('mt-0.5 shrink-0', tone.dot)} />
<span className="text-pretty">{d}</span>
</li>
))}
</ul>
{p.link && <Arrow link={p.link} className="ml-8 mt-2 text-[13.5px] text-foreground" />}
</div>
</div>
</div>
)}
</div>
);
}
/* ------------------------------------------------------------------ */
/* locale-suggest */
/* ------------------------------------------------------------------ */
function LocaleSuggest(p: ViewProps) {
const { t } = p;
const langs = p.languages ?? [];
const id = useId();
const [pick, setPick] = useState(0);
const target = langs[pick];
return (
<div className={cn('kit-rise border-b border-border bg-card', p.className)}>
<form
action={target?.href}
method="get"
onSubmit={(e) => {
if (!target) return;
e.preventDefault();
window.location.href = target.href;
}}
className="kit-container flex flex-col gap-3 py-3 sm:flex-row sm:items-center sm:gap-4"
>
<p className="flex min-w-0 flex-1 items-start gap-2.5 text-[14px] leading-snug text-foreground sm:items-center">
<Icon name={p.icon ?? 'languages'} size={18} className="mt-px shrink-0 text-primary sm:mt-0" />
<span className="text-pretty">{p.text}</span>
</p>
<div className="flex items-center gap-2">
<label htmlFor={id} className="sr-only">
{t.pick}
</label>
<span className="relative min-w-0 flex-1 sm:w-48 sm:flex-none">
<select
id={id}
value={pick}
onChange={(e) => setPick(Number(e.target.value))}
className="h-11 w-full cursor-pointer appearance-none rounded-lg border border-border bg-background pl-3 pr-9 text-base text-foreground transition-[border-color,box-shadow] hover:border-border-strong focus-visible:border-[color-mix(in_oklab,var(--primary)_50%,var(--border))] focus-visible:shadow-[0_0_0_4px_var(--glow)] focus-visible:outline-none sm:h-9 sm:text-[13.5px]"
>
{langs.map((l, i) => (
<option key={i} value={i}>
{l.label}
</option>
))}
</select>
<Icon name="chevrons-up-down" size={15} className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
</span>
<button type="submit" className="inline-flex h-11 shrink-0 cursor-pointer items-center gap-1.5 rounded-lg bg-primary px-4 text-[14px] font-medium text-primary-foreground transition-[filter,transform] hover:brightness-110 active:scale-[0.97] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-primary sm:h-9 sm:text-[13.5px]">
{t.go}
<Icon name="arrow-right" size={14} />
</button>
<button type="button" onClick={p.onClose} className="inline-flex h-11 shrink-0 cursor-pointer items-center rounded-lg px-3 text-[14px] font-medium text-muted-foreground transition-colors hover:bg-[color-mix(in_oklab,var(--foreground)_6%,transparent)] hover:text-foreground focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-primary sm:h-9 sm:text-[13.5px]">
{t.stay}
</button>
</div>
</form>
</div>
);
}
/* ------------------------------------------------------------------ */
const VIEWS: Record<AnnouncementBarVariant, (p: ViewProps) => React.ReactNode> = {
rotating: Rotating,
pill: Pill,
'launch-week': LaunchWeek,
notice: Notice,
'locale-suggest': LocaleSuggest,
};
export function AnnouncementBar({ variant = 'rotating', labels, ...rest }: AnnouncementBarProps) {
const t = useTexts(TEXTS, labels);
const [closed, setClosed] = useState(false);
const [leaving, setLeaving] = useState(false);
const close = () => {
if (prefersReducedMotion()) return setClosed(true);
setLeaving(true);
window.setTimeout(() => setClosed(true), 320);
};
if (closed) return null;
const View = VIEWS[variant] ?? Rotating;
return (
<div className={cn('grid transition-[grid-template-rows,opacity] duration-300 ease-[var(--ease-kit)]', leaving ? 'grid-rows-[0fr] opacity-0' : 'grid-rows-[1fr]')}>
<div className={cn('min-w-0', leaving && 'overflow-hidden')}>
<View {...rest} variant={variant} t={t} onClose={close} />
</div>
</div>
);
}Häufige Fragen
Wie installiere ich Ankündigungsleiste?
Mit aiw add @ai-website/announcement-bar. Der Befehl legt die Datei components/ui/announcement-bar.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind icon. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Ankündigungsleiste?
5 Varianten, Wechselnde Meldungen, Kapsel über der Überschrift, Launch-Woche, Geplanter Hinweis und Sprachvorschlag. Du wählst sie mit der Prop variant, zum Beispiel variant="pill".
Kann ich die Texte von Ankündigungsleiste übersetzen?
Ja. Ankündigungsleiste bringt 17 eigene Texte auf Deutsch und Englisch mit, etwa „Hinweis schließen“, „Vorherige Meldung“ und „Nächste Meldung“. Die Sprache folgt dem KitProvider, einzelne Texte ersetzt du über die Prop labels.
Kann ein KI-Assistent Ankündigungsleiste einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Ankündigungsleiste mit aiw_components und installieren sie mit aiw_add @ai-website/announcement-bar. Nenn im Prompt die Variante, etwa variant="rotating", dann schreibt der Assistent deine Inhalte direkt in die Props.