Zum Inhalt springen
UI Kit
Accent

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

2 Variants

Variants

Im Fließtext

Sparklines sitzen in der Textzeile und richten sich an ihr aus, jede mit eigenem Namen für Screenreader.

Kennzahlenreihe

Vier Kennzahlen, jede mit einer Sparkline über die ganze Breite. Fläche, Linie, Balken und eine ruhige graue Linie.

Details

Was Sparkline mitbringt, aus Registry und Quellcode gelesen.

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

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)

  • valueKeyoptional
    string
  • xKeyoptional
    string

    Key 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

  • formatoptional
    Intl.NumberFormatOptions

    Value format, e.g. { style: 'currency', currency: 'EUR' }

  • xFormatoptional
    Intl.DateTimeFormatOptions

    Tooltip format for date values

  • widthoptional
    number

    Width in px. Without it the sparkline fills its container (not in running text).

  • heightoptional
    number

    Height in px

  • toneoptional
    'accent' | 'muted'

    accent: the whole line in the accent. muted: grey line, only the last point in the accent.

  • highlightoptional
    number

    Bars: index of the bar that keeps the accent, the others step back. Default with tone muted: the last bar.

  • endDotoptional
    boolean

    Dot on the last value (line, area)

  • interactiveoptional
    boolean

    Tooltip and arrow keys. Default on.

  • inlineoptional
    boolean

    Sits 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
components/ui/sparkline.tsx
'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.

Oft gesucht

Direkt zu

In die Zwischenablage kopiert