Sparkline
Ein Diagramm in Wortgröße für Fließtext und Kennzahlen, als Linie, Fläche oder Balken, mit Tooltip und Pfeiltasten.
Eine von 6 Components in KPI-, Live- und Karten-Charts
Details
Was Sparkline mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- KPI-, Live- und Karten-Charts
- Bereich
- Elements
- Variants
- 2
- Version
- 1.0.0 · seit 1.0.0
- Datei
- components/ui/sparkline.tsx
- Bringt mit
- utilskitchart
- npm-Pakete
- Keine
- Sprachen
- Texte kommen über Props
- Studio
- Im Code anpassen
- Mit KI bauen
- Per MCP in Claude, Cursor, Copilot und Codex
Props
Die Schnittstelle SparklineProps aus components/ui/sparkline.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch. Nimmt außerdem alle Props von ChartA11y an.
dataPflicht(number | Datum)[]Numbers, or rows with valueKey (and xKey for dates in the tooltip)
valueKeyoptionalstringxKeyoptionalstringKey of the date or category in each row, e.g. "day" with "2026-09-14"
variantoptional'line' | 'area' | 'bar'line, area (line with a tint) or bar
formatoptionalIntl.NumberFormatOptionsValue format, e.g. { style: 'currency', currency: 'EUR' }
xFormatoptionalIntl.DateTimeFormatOptionsTooltip format for date values
widthoptionalnumberWidth in px. Without it the sparkline fills its container (not in running text).
heightoptionalnumberHeight in px
toneoptional'accent' | 'muted'accent: the whole line in the accent. muted: grey line, only the last point in the accent.
highlightoptionalnumberBars: index of the bar that keeps the accent, the others step back. Default with tone muted: the last bar.
endDotoptionalbooleanDot on the last value (line, area)
interactiveoptionalbooleanTooltip and arrow keys. Default on.
inlineoptionalbooleanSits in running text: renders inline and aligns with the text.
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- Sparkline reagiert selbst auf die Tasten Leertaste.
- Setzt aria-describedby, aria-label und aria-live, damit Screenreader Zweck und Zustand ansagen.
- Nutzt die Rolle img.
- Meldet Änderungen über eine Live-Region, ohne den Fokus zu verschieben.
- Ergänzt Text für Screenreader, wo Icons und Grafiken allein stehen.
- Kommt ohne Client-State aus. Der Server schreibt sie vollständig, sie ist auch ohne JavaScript lesbar.
Bewegung
- Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig. 2 Stellen im Code schalten Bewegung dann ab.
- Ohne Animationen, sie steht sofort ruhig da.
Quellcode
Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.
Datei anzeigen, 171 Zeilen
'use client';
import { useId } from 'react';
import { cn, round, useTexts } from '@/lib/kit';
import { CHART_TEXTS, ChartRoot, barPath, linePath, numberFormatter, useActive, useEntrance, useIntlLocale, useShown, useWidth, xFormatters, type ChartA11y, type Datum } from '@/components/ui/chart';
export interface SparklineProps extends ChartA11y {
/** Numbers, or rows with valueKey (and xKey for dates in the tooltip) */
data: (number | Datum)[];
valueKey?: string;
/** Key of the date or category in each row, e.g. "day" with "2026-09-14" */
xKey?: string;
/** line, area (line with a tint) or bar */
variant?: 'line' | 'area' | 'bar';
/** Value format, e.g. { style: 'currency', currency: 'EUR' } */
format?: Intl.NumberFormatOptions;
/** Tooltip format for date values */
xFormat?: Intl.DateTimeFormatOptions;
/** Width in px. Without it the sparkline fills its container (not in running text). */
width?: number;
/** Height in px */
height?: number;
/** accent: the whole line in the accent. muted: grey line, only the last point in the accent. */
tone?: 'accent' | 'muted';
/** Bars: index of the bar that keeps the accent, the others step back. Default with tone muted: the last bar. */
highlight?: number;
/** Dot on the last value (line, area) */
endDot?: boolean;
/** Tooltip and arrow keys. Default on. */
interactive?: boolean;
/** Sits in running text: renders inline and aligns with the text. */
inline?: boolean;
className?: string;
}
/**
* A word-sized chart for running text and stat tiles.
* <p>Visits rose <Sparkline inline width={64} data={[312, 340, 298, 402]} label="Visits, last 4 weeks" /> this month.</p>
*/
export function Sparkline({ data, valueKey = 'value', xKey, variant = 'line', format, xFormat, width: fixed, height = 28, tone = 'accent', highlight, endDot = true, interactive = true, inline = false, label, summary, labels, animate = true, className }: SparklineProps) {
const intl = useIntlLocale();
const t = useTexts(CHART_TEXTS, labels);
const fmt = numberFormatter(intl, format);
const uid = useId().replace(/[^a-zA-Z0-9]/g, '');
const [measureRef, measured] = useWidth<HTMLSpanElement>(fixed ?? 120);
const [rootRef, entrance] = useEntrance<HTMLDivElement>(!inline && animate);
const anim = inline ? undefined : entrance;
const values = data.map((d) => (typeof d === 'number' ? d : typeof d[valueKey] === 'number' ? (d[valueKey] as number) : 0));
const n = values.length;
const { active, setActive, onKeyDown, onFocus, onBlur } = useActive(n);
const { shown, open, jump } = useShown(active);
// Crosshair, dot and tooltip glide from value to value, they only appear in place
const glide = jump ? 'transition-opacity duration-200 motion-reduce:transition-none' : 'transition-[transform,opacity] duration-300 ease-[var(--ease-kit)] motion-reduce:transition-none';
const xf = xKey ? xFormatters(intl, (data[0] as Datum)?.[xKey], xFormat, xFormat) : null;
const xOf = (i: number) => (xf && typeof data[i] !== 'number' ? xf.tooltip((data[i] as Datum)[xKey!]) : '');
const w = fixed ?? measured;
const pad = variant === 'bar' ? 0 : 4;
const lo = variant === 'bar' ? Math.min(0, ...values) : Math.min(...values);
const hi = Math.max(...values);
const span = hi - lo || 1;
const y = (v: number) => round(pad + (1 - (v - lo) / span) * (height - pad * 2));
const step = variant === 'bar' ? w / Math.max(1, n) : (w - pad * 2) / Math.max(1, n - 1);
const x = (i: number) => round(variant === 'bar' ? step * (i + 0.5) : pad + step * i);
const pts = values.map((v, i) => [x(i), y(v)] as [number, number]);
const line = linePath(pts, true);
// Plain tokens instead of the chart variables, so an inline sparkline works without the chart styles
const accent = 'var(--primary)';
const soft = 'color-mix(in oklab, var(--primary) 34%, var(--background))';
const stroke = tone === 'muted' ? 'var(--muted-foreground)' : accent;
const first = values[0] ?? 0;
const last = values[n - 1] ?? 0;
const auto = n ? `${fmt(first)} → ${fmt(last)}, min ${fmt(Math.min(...values))}, max ${fmt(hi)}` : '';
const aria = `${label}. ${summary ?? auto}`;
const live = active !== null ? [xOf(active), fmt(values[active])].filter(Boolean).join(', ') : '';
const hintId = `${uid}-hint`;
const pick = (clientX: number, el: Element) => {
const r = el.getBoundingClientRect();
const p = ((clientX - r.left) / (r.width || w)) * w;
const i = variant === 'bar' ? Math.floor(p / step) : Math.round((p - pad) / (step || 1));
setActive(Math.max(0, Math.min(n - 1, i)));
};
const Tag = inline ? 'span' : 'div';
return (
<ChartRoot anim={anim} rootRef={rootRef} inline={inline} className={cn(inline ? 'inline-block align-middle' : 'w-full', className)}>
<Tag className={cn('relative', inline ? 'inline-block align-middle' : 'block w-full')} style={inline || fixed ? { width: w } : undefined}>
<span
ref={measureRef}
role="img"
aria-label={aria}
aria-describedby={interactive ? hintId : undefined}
tabIndex={interactive ? 0 : undefined}
onKeyDown={interactive ? onKeyDown : undefined}
onFocus={interactive ? onFocus : undefined}
onBlur={interactive ? onBlur : undefined}
onPointerMove={interactive ? (e) => pick(e.clientX, e.currentTarget) : undefined}
onPointerDown={interactive ? (e) => pick(e.clientX, e.currentTarget) : undefined}
onPointerLeave={interactive ? (e) => e.pointerType === 'mouse' && setActive(null) : undefined}
className={cn('block touch-pan-y rounded-[3px] outline-offset-2', interactive && 'cursor-crosshair')}
>
<svg width={w} height={height} viewBox={`0 0 ${w} ${height}`} aria-hidden className="block overflow-visible">
{variant === 'bar' ? (
values.map((v, i) => {
const bw = Math.max(1, Math.min(inline ? 5 : 8, step - 2));
const top = y(Math.max(0, v));
const h = Math.abs(y(v) - y(0));
const hi = highlight ?? (tone === 'muted' ? n - 1 : -1);
return (
<path
key={i}
d={barPath(x(i) - bw / 2, v >= 0 ? top : y(0), bw, Math.max(1, h), 1.5, v >= 0 ? 'up' : 'down')}
fill={hi < 0 || i === hi || active === i ? accent : soft}
opacity={active !== null && active !== i ? 0.45 : 1}
className={anim ? 'kitch-grow-y' : undefined}
style={anim ? ({ '--kitch-d': `${round((i / n) * 240, 0)}ms` } as React.CSSProperties) : undefined}
/>
);
})
) : (
<>
{variant === 'area' && (
<>
<defs>
<linearGradient id={`${uid}-a`} x1="0" x2="0" y1="0" y2="1">
<stop offset="0" stopColor={stroke} stopOpacity="0.22" />
<stop offset="1" stopColor={stroke} stopOpacity="0" />
</linearGradient>
</defs>
<path d={`${line} L${x(n - 1)} ${height} L${x(0)} ${height} Z`} fill={`url(#${uid}-a)`} className={anim ? 'kitch-clip' : undefined} />
</>
)}
<path d={line} fill="none" stroke={stroke} strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" pathLength={1} className={anim ? 'kitch-draw' : undefined} />
{endDot && n > 0 && <circle cx={x(n - 1)} cy={y(last)} r="2.75" fill={accent} stroke="var(--card)" strokeWidth="1.5" className={anim ? 'kitch-pop' : undefined} style={anim ? ({ '--kitch-d': '780ms' } as React.CSSProperties) : undefined} />}
{shown !== null && shown < n && (
<>
<line x1={0} x2={0} y1={0} y2={height} stroke="var(--border-strong)" strokeWidth="1" className={glide} style={{ transform: `translateX(${round(x(shown), 0) + 0.5}px)`, opacity: open ? 1 : 0 }} />
<circle cx={0} cy={0} r="3" fill={accent} stroke="var(--card)" strokeWidth="1.5" className={glide} style={{ transform: `translate(${x(shown)}px, ${y(values[shown])}px)`, opacity: open ? 1 : 0 }} />
</>
)}
</>
)}
</svg>
</span>
{interactive && (
<span
aria-hidden
className={cn(
'pointer-events-none absolute bottom-full left-0 z-10 mb-1.5 -translate-x-1/2 whitespace-nowrap rounded-md bg-foreground px-1.5 py-0.5 text-[11px] font-medium leading-5 tabular-nums text-background shadow-[var(--shadow-md)]',
glide,
open ? 'opacity-100' : 'opacity-0',
)}
style={{ transform: `translateX(${shown !== null ? x(shown) : 0}px)` }}
>
{shown !== null && shown < n && (xOf(shown) ? `${xOf(shown)} · ${fmt(values[shown])}` : fmt(values[shown]))}
</span>
)}
{interactive && (
<span id={hintId} className="sr-only">
{t.hint}
</span>
)}
<span aria-live="polite" className="sr-only">
{live}
</span>
</Tag>
</ChartRoot>
);
}Häufige Fragen
Wie installiere ich Sparkline?
Mit aiw add @ai-website/sparkline. Der Befehl legt die Datei components/ui/sparkline.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind chart. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Sparkline?
2 Varianten, Im Fließtext und Kennzahlenreihe.
Kann ein KI-Assistent Sparkline einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Sparkline mit aiw_components und installieren sie mit aiw_add @ai-website/sparkline. Nenn im Prompt die Variante, etwa variant="area", dann schreibt der Assistent deine Inhalte direkt in die Props.