Firmengeschichte
Die Geschichte eines Unternehmens, groß erzählt. Epochen mit einer Linie, die sich füllt, eine Jahreszahl, die mitzählt, ein Jahreslineal zum Ziehen, Jahrzehnt-Kacheln und das Jubiläum mit riesiger Zahl. Alte Bilder erscheinen auf Wunsch als Archivabzug.
Eine von 2 Components in About
Variants
Details
Was Firmengeschichte mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- About
- Bereich
- Content
- Variants
- 5
- Version
- 1.0.0 · seit 1.1.0
- Datei
- components/ui/history-timeline.tsx
- Bringt mit
- utilskiticonnumber-tickerrevealmotionsection
- npm-Pakete
- Keine
- Sprachen
- Englisch und Deutsch, 9 eigene Texte
- Mit KI bauen
- Per MCP in Claude, Cursor, Copilot und Codex
Props
Die Schnittstelle HistoryTimelineProps aus components/ui/history-timeline.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.
variantoptionalHistoryTimelineVarianterasyear-countertrackdecadesanniversary
eyebrowoptionalReact.ReactNodetitleoptionalReact.ReactNodedescriptionoptionalReact.ReactNodeeventsoptionalHistoryEvent[]erasoptionalHistoryEra[]foundedoptionalnumberanniversary: the founding year, the current year is computed from `now`
nowoptionalnumberanniversary: the year of the jubilee, e.g. 2026
quoteoptional{ text: string; author: string; role?: string; media?: React.ReactNode }anniversary: a sentence from the founders
Eingebaute Texte
Diese Texte schreibt Firmengeschichte selbst. Die Sprache folgt dem KitProvider, über die Prop labels ersetzt du einzelne.
Alle 9 Texte anzeigen
| Schlüssel | Englisch | Deutsch |
|---|---|---|
| years | Englisch years | Deutsch Jahre |
| previous | Englisch Previous year | Deutsch Vorheriges Jahr |
| next | Englisch Next year | Deutsch Nächstes Jahr |
| timeline | Englisch Years | Deutsch Jahre |
| since | Englisch since {y} | Deutsch seit {y} |
| from | Englisch Since {y} | Deutsch Seit {y} |
| of | Englisch {a} of {b} | Deutsch {a} von {b} |
| milestones | Englisch Milestones | Deutsch Meilensteine |
| decade | Englisch The {d}s | Deutsch Die {d}er |
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- Firmengeschichte reagiert selbst auf die Tasten Pfeil links, Pfeil rechts, Pos1 und Ende.
- Setzt aria-controls, aria-label, aria-labelledby, aria-live und aria-selected, damit Screenreader Zweck und Zustand ansagen.
- Nutzt die Rollen tab, tablist und tabpanel.
- Meldet Änderungen über eine Live-Region, ohne den Fokus zu verschieben.
- Ergänzt Text für Screenreader, wo Icons und Grafiken allein stehen.
- Der Server rendert den Startzustand vollständig, JavaScript ergänzt nur die Interaktion.
Bewegung
- Erscheint beim ersten Sichtkontakt mit einem kurzen Auftritt (kit-rise, useInView).
- Bewegt sich mit dem Scrollen, der Fortschritt folgt der Position auf der Seite.
- Bringt eigene Animationen mit, ruhig getaktet und ohne Wackeln.
- Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig. 6 Stellen im Code schalten Bewegung dann ab.
Quellcode
Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.
Datei anzeigen, 549 Zeilen
'use client';
import { useEffect, useId, useRef, useState } from 'react';
import { cn, round, useLocale, useTexts } from '@/lib/kit';
import { Icon } from '@/components/ui/icon';
import { Parallax, Stagger, useScrollFade } from '@/components/ui/motion';
import { NumberTicker } from '@/components/ui/number-ticker';
import { Reveal } from '@/components/ui/reveal';
import { Eyebrow } from '@/components/ui/section';
/*
* The history of a company, told large. Years are data, pictures are nodes,
* old pictures can be shown as archive prints.
*
* eras the history in chapters, each era with its picture and a line
* that fills while you read through it
* year-counter a large year stands still and counts along while the events
* pass beside it
* track a ruler of years to drag, swipe or step through, the chosen
* year opens above it
* decades one tile per decade with its picture and milestones, the
* years count up once
* anniversary the jubilee: the number of years large, the founding quote and
* a strip of milestones that draws itself
*/
export type HistoryTimelineVariant = 'eras' | 'year-counter' | 'track' | 'decades' | 'anniversary';
export interface HistoryEvent {
year: number;
title: string;
text?: string;
media?: React.ReactNode;
/** Show the picture as an archive print, black and white with grain */
archive?: boolean;
/** A number that belongs to the year, e.g. 40 employees */
figure?: { value: string; label: string };
}
export interface HistoryEra {
/** e.g. 1951 to 1969 */
from: number;
to?: number;
title: string;
text?: string;
media?: React.ReactNode;
archive?: boolean;
events: HistoryEvent[];
}
export interface HistoryTimelineProps {
variant?: HistoryTimelineVariant;
eyebrow?: React.ReactNode;
title?: React.ReactNode;
description?: React.ReactNode;
events?: HistoryEvent[];
eras?: HistoryEra[];
/** anniversary: the founding year, the current year is computed from `now` */
founded?: number;
/** anniversary: the year of the jubilee, e.g. 2026 */
now?: number;
/** anniversary: a sentence from the founders */
quote?: { text: string; author: string; role?: string; media?: React.ReactNode };
labels?: Partial<Texts>;
className?: string;
}
const TEXTS = {
en: {
years: 'years',
previous: 'Previous year',
next: 'Next year',
timeline: 'Years',
since: 'since {y}',
from: 'Since {y}',
of: '{a} of {b}',
milestones: 'Milestones',
decade: 'The {d}s',
},
de: {
years: 'Jahre',
previous: 'Vorheriges Jahr',
next: 'Nächstes Jahr',
timeline: 'Jahre',
since: 'seit {y}',
from: 'Seit {y}',
of: '{a} von {b}',
milestones: 'Meilensteine',
decade: 'Die {d}er',
},
};
type Texts = (typeof TEXTS)['en'];
/* ------------------------------------------------------------------ */
/* Shared pieces */
/* ------------------------------------------------------------------ */
/** A picture, old ones as archive prints: grey, a touch warm, a little grain */
function Picture({ media, archive, aspect = '4 / 3', className, parallax }: { media?: React.ReactNode; archive?: boolean; aspect?: string; className?: string; parallax?: boolean }) {
if (!media) return null;
const inner = <div className={cn('size-full [&>*]:size-full', archive && 'contrast-[1.05] grayscale-[0.92] sepia-[0.18]')}>{media}</div>;
return (
<div data-clip-ok className={cn('relative overflow-hidden rounded-xl bg-muted', className)} style={{ aspectRatio: aspect }}>
{parallax ? (
<Parallax speed={36} className="size-full scale-[1.1]">
{inner}
</Parallax>
) : (
inner
)}
{archive && (
<span aria-hidden className="pointer-events-none absolute inset-0 opacity-[0.16] mix-blend-multiply [background-image:radial-gradient(rgb(0_0_0/0.6)_0.6px,transparent_0.8px)] [background-size:3px_3px]" />
)}
</div>
);
}
function Head({ eyebrow, title, description, center }: { eyebrow?: React.ReactNode; title?: React.ReactNode; description?: React.ReactNode; center?: boolean }) {
if (!title && !eyebrow) return null;
return (
<div className={cn('mb-12 max-w-[42rem] sm:mb-16', center && 'mx-auto text-center')}>
{eyebrow && <Eyebrow className="kit-rise">{eyebrow}</Eyebrow>}
{title && <h2 className="kit-rise text-balance text-h2 font-semibold text-foreground" style={{ '--kit-delay': '60ms' } as React.CSSProperties}>{title}</h2>}
{description && <p className={cn('kit-rise mt-4 text-pretty text-lead text-muted-foreground', center && 'mx-auto max-w-[36rem]')} style={{ '--kit-delay': '120ms' } as React.CSSProperties}>{description}</p>}
</div>
);
}
/** 0 to 1 while the element passes a line at 45 % of the window */
function useFill<T extends HTMLElement>() {
const ref = useRef<T>(null);
const [v, setV] = useState(1);
useEffect(() => {
const el = ref.current;
if (!el || window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
let frame = 0;
const update = () => {
frame = 0;
const r = el.getBoundingClientRect();
const line = window.innerHeight * 0.45;
// At the end of the page the bottom never reaches the line, the fill completes once it is in view
if (window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 2 && r.bottom <= window.innerHeight) return setV(1);
setV(round(Math.min(1, Math.max(0, (line - r.top) / Math.max(1, r.height))), 3));
};
const on = () => {
if (!frame) frame = requestAnimationFrame(update);
};
update();
window.addEventListener('scroll', on, { passive: true });
window.addEventListener('resize', on);
return () => {
cancelAnimationFrame(frame);
window.removeEventListener('scroll', on);
window.removeEventListener('resize', on);
};
}, []);
return [ref, v] as const;
}
/* ------------------------------------------------------------------ */
/* eras */
/* ------------------------------------------------------------------ */
function EraBlock({ era, i, t }: { era: HistoryEra; i: number; t: Texts }) {
const [ref, fill] = useFill<HTMLDivElement>();
const flip = i % 2 === 1;
return (
<section className="border-t border-border py-14 sm:py-16 lg:py-20">
<div className="grid grid-cols-[minmax(0,1fr)] gap-10 lg:grid-cols-2 lg:gap-16">
<div className={cn('lg:sticky lg:top-12 lg:self-start', flip && 'lg:order-last')}>
<Reveal effect="rise">
<p className="text-[15px] font-semibold tabular-nums text-[color-mix(in_oklab,var(--primary)_80%,var(--foreground))]">
{era.to ? `${era.from}–${era.to}` : t.from.replace('{y}', String(era.from))}
</p>
<h3 className="mt-3 text-balance text-[30px] font-semibold leading-[1.12] tracking-[-0.02em] text-foreground sm:text-[38px]">{era.title}</h3>
{era.text && <p className="mt-4 max-w-[34rem] text-pretty text-[17px] leading-[1.65] text-muted-foreground">{era.text}</p>}
</Reveal>
<Picture media={era.media} archive={era.archive} aspect="16 / 10" className="mt-8" parallax />
</div>
<div ref={ref} className="relative pl-8 sm:pl-10">
<span aria-hidden className="absolute bottom-2 left-[5px] top-2 w-px bg-border" />
<span aria-hidden className="absolute bottom-2 left-[5px] top-2 w-px origin-top bg-primary transition-transform duration-150 ease-linear motion-reduce:transition-none" style={{ transform: `scaleY(${fill})` }} />
<ol className="space-y-10">
{era.events.map((e, j) => (
<Reveal as="li" key={j} effect="rise" className="relative">
<span aria-hidden className="absolute -left-8 top-[7px] size-[11px] rounded-full border-2 border-primary bg-background sm:-left-10" />
<p className="text-[14px] font-semibold tabular-nums text-foreground">{e.year}</p>
<h4 className="mt-1 text-pretty text-[19px] font-semibold leading-[1.35] text-foreground">{e.title}</h4>
{e.text && <p className="mt-2 text-pretty text-[16px] leading-[1.65] text-muted-foreground">{e.text}</p>}
{e.figure && (
<p className="mt-3 inline-flex items-baseline gap-2 rounded-lg border border-border bg-surface px-3 py-1.5">
<span className="text-[16px] font-semibold tabular-nums text-foreground">{e.figure.value}</span>
<span className="text-[13px] text-muted-foreground">{e.figure.label}</span>
</p>
)}
</Reveal>
))}
</ol>
</div>
</div>
</section>
);
}
function Eras(p: HistoryTimelineProps & { t: Texts }) {
return (
<section className={cn('py-16 sm:py-20 lg:py-24', p.className)}>
<div className="kit-container">
<Head eyebrow={p.eyebrow} title={p.title} description={p.description} />
{(p.eras ?? []).map((era, i) => (
<EraBlock key={i} era={era} i={i} t={p.t} />
))}
</div>
</section>
);
}
/* ------------------------------------------------------------------ */
/* year-counter */
/* ------------------------------------------------------------------ */
function YearCounter(p: HistoryTimelineProps & { t: Texts }) {
const events = p.events ?? [];
const [active, setActive] = useState(0);
const listRef = useRef<HTMLOListElement>(null);
useEffect(() => {
let frame = 0;
const update = () => {
frame = 0;
const items = [...(listRef.current?.children ?? [])] as HTMLElement[];
const line = window.innerHeight * 0.5;
let cur = 0;
items.forEach((el, i) => {
if (el.getBoundingClientRect().top <= line) cur = i;
});
const last = items[items.length - 1];
if (last && window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 2 && last.getBoundingClientRect().top < window.innerHeight) cur = items.length - 1;
setActive(cur);
};
const on = () => {
if (!frame) frame = requestAnimationFrame(update);
};
update();
window.addEventListener('scroll', on, { passive: true });
window.addEventListener('resize', on);
return () => {
cancelAnimationFrame(frame);
window.removeEventListener('scroll', on);
window.removeEventListener('resize', on);
};
}, [events.length]);
const e = events[active];
const first = events[0]?.year ?? 0;
const last = events[events.length - 1]?.year ?? first;
const share = last > first && e ? (e.year - first) / (last - first) : 1;
const digits = String(e?.year ?? '').split('');
return (
<section className={cn('py-16 sm:py-20 lg:py-24', p.className)}>
<div className="kit-container">
<Head eyebrow={p.eyebrow} title={p.title} description={p.description} />
<div className="grid grid-cols-[minmax(0,1fr)] gap-x-16 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
{/* The year stands still, on phones as a bar at the top */}
<div className="sticky top-0 z-10 -mx-6 border-b border-border bg-[color-mix(in_oklab,var(--background)_90%,transparent)] px-6 py-3 backdrop-blur-md lg:top-16 lg:mx-0 lg:self-start lg:border-0 lg:bg-transparent lg:p-0 lg:backdrop-blur-none">
<p aria-live="polite" className="flex items-baseline gap-4 overflow-hidden lg:block">
<span className="sr-only">{e?.year}</span>
<span aria-hidden className="flex text-[44px] font-semibold leading-none tracking-[-0.045em] text-foreground tabular-nums sm:text-[56px] lg:text-[clamp(7rem,4rem+6vw,11rem)] lg:leading-[0.85]">
{digits.map((d, i) => (
<span key={i} className="relative inline-block h-[1em] overflow-hidden">
<span className="block transition-transform duration-700 ease-[var(--ease-kit)] motion-reduce:transition-none" style={{ transform: `translateY(${-Number(d) * 10}%)` }}>
{'0123456789'.split('').map((n) => (
<span key={n} className="block h-[1em]">
{n}
</span>
))}
</span>
</span>
))}
</span>
<span className="min-w-0 truncate text-[15px] font-medium text-muted-foreground lg:mt-6 lg:block lg:text-[17px]">{e?.title}</span>
</p>
<div className="mt-3 h-0.5 overflow-hidden rounded-full bg-border lg:mt-8 lg:max-w-[22rem]" aria-hidden>
<div className="h-full origin-left bg-primary transition-transform duration-700 ease-[var(--ease-kit)] motion-reduce:transition-none" style={{ transform: `scaleX(${round(share, 3)})` }} />
</div>
<div className="mt-2 hidden max-w-[22rem] justify-between text-[13px] tabular-nums text-muted-foreground lg:flex" aria-hidden>
<span>{first}</span>
<span>{last}</span>
</div>
</div>
<ol ref={listRef} className="pt-10 lg:pt-0">
{events.map((ev, i) => (
<li key={i} className={cn('border-b border-border py-10 transition-opacity duration-500 first:pt-0 lg:min-h-[30rem] lg:py-14', active === i ? 'opacity-100' : 'opacity-40 motion-reduce:opacity-100')}>
<p className="text-[14px] font-semibold tabular-nums text-[color-mix(in_oklab,var(--primary)_80%,var(--foreground))]">{ev.year}</p>
<h3 className="mt-2 text-balance text-[24px] font-semibold leading-[1.2] tracking-[-0.015em] text-foreground sm:text-[28px]">{ev.title}</h3>
{ev.text && <p className="mt-3 max-w-[34rem] text-pretty text-[16.5px] leading-[1.65] text-muted-foreground">{ev.text}</p>}
<Picture media={ev.media} archive={ev.archive} aspect="16 / 10" className="mt-6 max-w-[34rem]" />
</li>
))}
</ol>
</div>
</div>
</section>
);
}
/* ------------------------------------------------------------------ */
/* track */
/* ------------------------------------------------------------------ */
function Track(p: HistoryTimelineProps & { t: Texts }) {
const { t } = p;
const uid = useId().replace(/:/g, '');
const events = p.events ?? [];
const [i, setI] = useState(0);
const [rowRef, fade] = useScrollFade<HTMLDivElement>();
const e = events[i];
const go = (n: number) => setI(Math.min(events.length - 1, Math.max(0, n)));
// Keep the chosen year in view in the ruler
useEffect(() => {
const row = rowRef.current;
const b = row?.querySelector<HTMLElement>(`[data-i="${i}"]`);
if (row && b) row.scrollTo({ left: b.offsetLeft - row.clientWidth / 2 + b.offsetWidth / 2, behavior: 'smooth' });
}, [i, rowRef]);
// Drag the ruler with a mouse, touch scrolls natively
const drag = useRef<{ x: number; left: number } | null>(null);
const key = (ev: React.KeyboardEvent) => {
if (ev.key === 'ArrowRight') {
ev.preventDefault();
go(i + 1);
}
if (ev.key === 'ArrowLeft') {
ev.preventDefault();
go(i - 1);
}
if (ev.key === 'Home') go(0);
if (ev.key === 'End') go(events.length - 1);
};
return (
<section className={cn('overflow-x-clip py-16 sm:py-20 lg:py-24', p.className)}>
<div className="kit-container">
<Head eyebrow={p.eyebrow} title={p.title} description={p.description} />
{e && (
<div key={i} id={`${uid}-panel`} role="tabpanel" aria-labelledby={`${uid}-y${i}`} className="grid grid-cols-[minmax(0,1fr)] items-center gap-8 md:grid-cols-[minmax(0,1.1fr)_minmax(0,1fr)] lg:gap-14">
<Picture media={e.media} archive={e.archive} aspect="16 / 10" className="kit-rise" />
<div className="kit-rise" style={{ '--kit-delay': '80ms' } as React.CSSProperties}>
<p className="text-[64px] font-semibold leading-none tracking-[-0.045em] text-foreground tabular-nums sm:text-[88px]">{e.year}</p>
<h3 className="mt-4 text-balance text-[24px] font-semibold leading-[1.2] tracking-[-0.015em] text-foreground sm:text-[28px]">{e.title}</h3>
{e.text && <p className="mt-3 text-pretty text-[16.5px] leading-[1.65] text-muted-foreground">{e.text}</p>}
{e.figure && (
<p className="mt-5 flex items-baseline gap-2">
<span className="text-[26px] font-semibold tabular-nums text-foreground">{e.figure.value}</span>
<span className="text-[14px] text-muted-foreground">{e.figure.label}</span>
</p>
)}
</div>
</div>
)}
<div className="mt-12 flex items-center gap-3 sm:mt-14">
<button type="button" onClick={() => go(i - 1)} disabled={i === 0} aria-label={t.previous} className="grid size-11 shrink-0 place-items-center rounded-full border border-border text-foreground transition-[background-color,opacity] hover:bg-accent disabled:opacity-35">
<Icon name="arrow-left" size={17} />
</button>
<div
ref={rowRef}
onPointerDown={(ev) => {
if (ev.pointerType !== 'mouse' || !rowRef.current) return;
drag.current = { x: ev.clientX, left: rowRef.current.scrollLeft };
}}
onPointerMove={(ev) => {
if (!drag.current || !rowRef.current) return;
rowRef.current.scrollLeft = drag.current.left - (ev.clientX - drag.current.x);
}}
onPointerUp={() => (drag.current = null)}
onPointerLeave={() => (drag.current = null)}
className={cn('kit-no-scrollbar relative min-w-0 flex-1 cursor-grab overflow-x-auto active:cursor-grabbing', fade)}
>
<div role="tablist" aria-label={t.timeline} onKeyDown={key} className="relative flex w-max items-end gap-0 px-[40%] pb-1 pt-2">
<span aria-hidden className="absolute inset-x-0 bottom-[22px] h-px bg-border" />
{events.map((ev, j) => {
const gap = j > 0 ? Math.min(160, Math.max(56, (ev.year - events[j - 1].year) * 9)) : 0;
return (
<button
key={j}
type="button"
role="tab"
id={`${uid}-y${j}`}
data-i={j}
aria-selected={i === j}
aria-controls={`${uid}-panel`}
tabIndex={i === j ? 0 : -1}
onClick={() => go(j)}
className="group/y relative flex shrink-0 flex-col items-center gap-2 px-2 pb-0"
style={{ marginLeft: gap }}
>
<span className={cn('text-[14px] font-semibold tabular-nums transition-[color,transform] duration-300', i === j ? '-translate-y-0.5 text-foreground' : 'text-muted-foreground group-hover/y:text-foreground')}>{ev.year}</span>
<span className="relative grid h-11 w-6 place-items-center">
<span className={cn('block rounded-full transition-[width,height,background-color] duration-300', i === j ? 'size-3.5 bg-primary shadow-[0_0_0_5px_color-mix(in_oklab,var(--primary)_22%,transparent)]' : 'size-2 bg-border-strong group-hover/y:bg-foreground')} />
</span>
</button>
);
})}
</div>
</div>
<button type="button" onClick={() => go(i + 1)} disabled={i === events.length - 1} aria-label={t.next} className="grid size-11 shrink-0 place-items-center rounded-full border border-border text-foreground transition-[background-color,opacity] hover:bg-accent disabled:opacity-35">
<Icon name="arrow-right" size={17} />
</button>
</div>
<p className="mt-3 text-center text-[13px] tabular-nums text-muted-foreground" aria-live="polite">
{t.of.replace('{a}', String(i + 1)).replace('{b}', String(events.length))}
</p>
</div>
</section>
);
}
/* ------------------------------------------------------------------ */
/* decades */
/* ------------------------------------------------------------------ */
function Decades(p: HistoryTimelineProps & { t: Texts }) {
const { t } = p;
const locale = useLocale();
const events = p.events ?? [];
const groups = new Map<number, HistoryEvent[]>();
for (const e of events) {
const d = Math.floor(e.year / 10) * 10;
groups.set(d, [...(groups.get(d) ?? []), e]);
}
const list = [...groups.entries()];
return (
<section className={cn('py-16 sm:py-20 lg:py-24', p.className)}>
<div className="kit-container">
<Head eyebrow={p.eyebrow} title={p.title} description={p.description} />
<Stagger step={70} className="grid grid-cols-[minmax(0,1fr)] gap-4 sm:grid-cols-2 lg:grid-cols-4">
{list.map(([d, evs]) => {
const lead = evs.find((x) => x.media) ?? evs[0];
return (
<article key={d} className="group/d flex flex-col overflow-hidden rounded-2xl border border-border bg-card shadow-[var(--highlight),var(--shadow-sm)] transition-[transform,box-shadow,border-color] duration-500 ease-[var(--ease-kit)] hover:-translate-y-1 hover:border-border-strong hover:shadow-[var(--highlight),var(--shadow-md)]">
<div className="relative overflow-hidden">
<div className="transition-transform duration-700 ease-[var(--ease-kit)] group-hover/d:scale-[1.04]">
<Picture media={lead.media} archive={lead.archive} aspect="16 / 10" className="rounded-none" />
</div>
<p className="absolute bottom-3 left-4 rounded-md bg-[color-mix(in_oklab,var(--background)_82%,transparent)] px-2.5 py-1 text-[13px] font-semibold text-foreground backdrop-blur">
{t.decade.replace('{d}', locale === 'de' ? String(d) : String(d).slice(-2) === '00' ? String(d) : `’${String(d).slice(-2)}`)}
</p>
</div>
<div className="flex flex-1 flex-col p-5 sm:p-6">
<p className="text-[44px] font-semibold leading-none tracking-[-0.04em] text-foreground tabular-nums">
<NumberTicker value={d} from={d - 9} duration={1100} format={{ useGrouping: false }} />
</p>
<ul className="mt-5 space-y-3.5">
{evs.map((e, j) => (
<li key={j} className="grid grid-cols-[3rem_minmax(0,1fr)] gap-3">
<span className="pt-px text-[13.5px] font-semibold tabular-nums text-[color-mix(in_oklab,var(--primary)_80%,var(--foreground))]">{e.year}</span>
<span>
<span className="block text-pretty text-[15.5px] font-semibold leading-[1.4] text-foreground">{e.title}</span>
{e.text && <span className="mt-1 block text-pretty text-[14px] leading-[1.55] text-muted-foreground">{e.text}</span>}
</span>
</li>
))}
</ul>
</div>
</article>
);
})}
</Stagger>
</div>
</section>
);
}
/* ------------------------------------------------------------------ */
/* anniversary */
/* ------------------------------------------------------------------ */
function Anniversary(p: HistoryTimelineProps & { t: Texts }) {
const { t } = p;
const events = p.events ?? [];
const founded = p.founded ?? events[0]?.year ?? 0;
const age = (p.now ?? events[events.length - 1]?.year ?? founded) - founded;
const [ref, fill] = useFill<HTMLDivElement>();
return (
<section className={cn('kit-glow relative overflow-x-clip py-16 sm:py-20 lg:py-28', p.className)}>
<div className="kit-container relative">
<div className="grid grid-cols-[minmax(0,1fr)] items-end gap-10 lg:grid-cols-[minmax(0,1.1fr)_minmax(0,1fr)] lg:gap-16">
<div>
{p.eyebrow && <Eyebrow className="kit-rise">{p.eyebrow}</Eyebrow>}
<p className="kit-rise flex items-end gap-4" style={{ '--kit-delay': '60ms' } as React.CSSProperties}>
<span className="kit-gradient-text text-[clamp(7rem,3rem+14vw,15rem)] font-semibold leading-[0.8] tracking-[-0.06em] tabular-nums">
<NumberTicker value={age} duration={1600} />
</span>
<span className="pb-[0.6em] text-[22px] font-semibold leading-tight text-foreground sm:text-[28px]">
{t.years}
<span className="block text-[15px] font-medium text-muted-foreground sm:text-[17px]">{t.since.replace('{y}', String(founded))}</span>
</span>
</p>
{p.title && <h2 className="kit-rise mt-8 max-w-[34rem] text-balance text-[28px] font-semibold leading-[1.15] tracking-[-0.02em] text-foreground sm:text-[36px]" style={{ '--kit-delay': '120ms' } as React.CSSProperties}>{p.title}</h2>}
{p.description && <p className="kit-rise mt-4 max-w-[34rem] text-pretty text-lead text-muted-foreground" style={{ '--kit-delay': '160ms' } as React.CSSProperties}>{p.description}</p>}
</div>
{p.quote && (
<Reveal effect="rise" delay={150} className="kit-card relative p-6 sm:p-8">
<Icon name="quote" size={26} className="text-primary" />
<blockquote className="mt-4 text-pretty text-[19px] font-medium leading-[1.5] tracking-[-0.01em] text-foreground sm:text-[21px]">{p.quote.text}</blockquote>
<div className="mt-6 flex items-center gap-3">
{p.quote.media && (
<span data-clip-ok className="size-12 shrink-0 overflow-hidden rounded-full border border-border grayscale-[0.9] sepia-[0.15] [&>*]:size-full">
{p.quote.media}
</span>
)}
<p className="text-[14px] leading-snug">
<span className="block font-semibold text-foreground">{p.quote.author}</span>
{p.quote.role && <span className="text-muted-foreground">{p.quote.role}</span>}
</p>
</div>
</Reveal>
)}
</div>
<div ref={ref} className="relative mt-16 sm:mt-20">
<h3 className="sr-only">{t.milestones}</h3>
<span aria-hidden className="absolute left-0 right-0 top-[7px] hidden h-px bg-border lg:block" />
<span aria-hidden className="absolute left-0 right-0 top-[7px] hidden h-px origin-left bg-primary transition-transform duration-150 ease-linear motion-reduce:transition-none lg:block" style={{ transform: `scaleX(${round(Math.min(1, fill * 1.6), 3)})` }} />
<span aria-hidden className="absolute bottom-0 left-[7px] top-0 w-px bg-border lg:hidden" />
<span aria-hidden className="absolute bottom-0 left-[7px] top-0 w-px origin-top bg-primary transition-transform duration-150 ease-linear motion-reduce:transition-none lg:hidden" style={{ transform: `scaleY(${round(Math.min(1, fill * 1.2), 3)})` }} />
<ol className="grid grid-cols-[minmax(0,1fr)] gap-8 lg:grid-cols-6 lg:gap-6">
{events.map((e, i) => (
<Reveal as="li" key={i} effect="rise" delay={i * 70} className="relative pl-8 lg:pl-0 lg:pt-8">
<span aria-hidden className="absolute left-0 top-0.5 size-[15px] rounded-full border-2 border-primary bg-background lg:top-0" />
<p className="text-[15px] font-semibold tabular-nums text-foreground">{e.year}</p>
<p className="mt-1.5 text-pretty text-[15px] leading-[1.5] text-muted-foreground">{e.title}</p>
</Reveal>
))}
</ol>
</div>
</div>
</section>
);
}
/* ------------------------------------------------------------------ */
export function HistoryTimeline(props: HistoryTimelineProps) {
const t = useTexts(TEXTS, props.labels);
const { variant = 'eras' } = props;
if (variant === 'year-counter') return <YearCounter {...props} t={t} />;
if (variant === 'track') return <Track {...props} t={t} />;
if (variant === 'decades') return <Decades {...props} t={t} />;
if (variant === 'anniversary') return <Anniversary {...props} t={t} />;
return <Eras {...props} t={t} />;
}Häufige Fragen
Wie installiere ich Firmengeschichte?
Mit aiw add @ai-website/history-timeline. Der Befehl legt die Datei components/ui/history-timeline.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind icon, number-ticker, reveal, motion und section. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Firmengeschichte?
5 Varianten, Epochen, Jahreszähler, Jahreslineal, Jahrzehnte und Jubiläum. Du wählst sie mit der Prop variant, zum Beispiel variant="year-counter".
Kann ich die Texte von Firmengeschichte übersetzen?
Ja. Firmengeschichte bringt 9 eigene Texte auf Deutsch und Englisch mit, etwa „Jahre“, „Vorheriges Jahr“ und „Nächstes Jahr“. Die Sprache folgt dem KitProvider, einzelne Texte ersetzt du über die Prop labels.
Kann ein KI-Assistent Firmengeschichte einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Firmengeschichte mit aiw_components und installieren sie mit aiw_add @ai-website/history-timeline. Nenn im Prompt die Variante, etwa variant="eras", dann schreibt der Assistent deine Inhalte direkt in die Props.
Kann ich Firmengeschichte ohne Code anpassen?
Ja, im Studio. Alle 5 Varianten lassen sich dort bearbeiten, Texte, Listen und Schalter. Das Studio schreibt den passenden HistoryTimeline-Code zum Kopieren.