Zum Inhalt springen
UI Kit
Accent

Scroll-Zeichnung

Eine Strichzeichnung, die sich beim Scrollen zeichnet und beim Zurückscrollen wieder verschwindet. Vier Zeichnungen dabei, eigene SVG-Pfade gehen auch.

Eine von 4 Components in Mikroanimationen

4 Variants

Variants

Bauplan

Bodenlinie, Wände, Dach, Tür, Fenster und zuletzt die Maße, in dieser Reihenfolge.

Route

Erst Höhenlinien und ein Fluss, dann der Weg vom Startpunkt bis zum Gipfel.

Wachstumslinie

Achsen und Raster, dann die Linie, dann die Meilensteine mit Beschriftung.

Arbeitsplatz

Stillleben eines Schreibtischs, Stück für Stück gezeichnet.

Details

Was Scroll-Zeichnung mitbringt, aus Registry und Quellcode gelesen.

Bereich
Visuals
Variants
4
Version
1.0.0 · seit 1.0.0
Datei
components/ui/scroll-drawing.tsx
Bringt mit
utilskitmotion
npm-Pakete
Keine
Sprachen
Texte kommen über Props
Studio
Im Code anpassen

Props

Die Schnittstelle ScrollDrawingProps aus components/ui/scroll-drawing.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.

  • drawingoptional
    DrawingName

    houseroutegrowthworkspace

    A built-in drawing, or pass your own strokes

  • strokesoptional
    DrawingStroke[]
  • viewBoxoptional
    string

    viewBox of your own strokes. Default "0 0 800 500".

  • startoptional
    number

    Scroll progress where drawing starts and where it is complete (0 = element enters, 1 = element leaves). Default 0.1 and 0.55.

  • endoptional
    number
  • labeloptional
    string

    Accessible description

Barrierefreiheit

Aus dem Quellcode der Component abgeleitet.

  • Setzt aria-label, damit Screenreader Zweck und Zustand ansagen.
  • Nutzt die Rolle label.
  • Kommt ohne Client-State aus. Der Server schreibt sie vollständig, sie ist auch ohne JavaScript lesbar.

Bewegung

  • Ohne Animationen, sie steht sofort ruhig da.

Quellcode

Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.

Datei anzeigen, 205 Zeilen
components/ui/scroll-drawing.tsx
'use client';

import { cn, round } from '@/lib/kit';
import { useScrollProgress } from '@/components/ui/motion';

/*
 * A line drawing that draws itself while you scroll. Every stroke has a
 * window of the scroll progress in which it goes from nothing to complete
 * (stroke-dashoffset), labels fade in once their stroke is done. Scrolling
 * back undraws it. Without JavaScript and with reduced motion the drawing is
 * complete. Four drawings come with it, any SVG paths work.
 */

export interface DrawingStroke {
  d: string;
  /** line (default), accent or soft */
  tone?: 'line' | 'accent' | 'soft';
  width?: number;
  /** Order of drawing. Strokes with the same step draw together. Default: their position. */
  step?: number;
  /** Dashed, for guides and dimensions */
  dashed?: boolean;
}

export interface DrawingLabel {
  x: number;
  y: number;
  text: string;
  /** Step after which the label appears */
  step: number;
  anchor?: 'start' | 'middle' | 'end';
  tone?: 'line' | 'accent';
}

export type DrawingName = 'house' | 'route' | 'growth' | 'workspace';

export interface ScrollDrawingProps {
  /** A built-in drawing, or pass your own strokes */
  drawing?: DrawingName;
  strokes?: DrawingStroke[];
  /** Labels for your own strokes, or texts for the built-in labels in order */
  labels?: DrawingLabel[] | string[];
  /** viewBox of your own strokes. Default "0 0 800 500". */
  viewBox?: string;
  /** Scroll progress where drawing starts and where it is complete (0 = element enters, 1 = element leaves). Default 0.1 and 0.55. */
  start?: number;
  end?: number;
  /** Accessible description */
  label?: string;
  className?: string;
}

const L = (d: string, tone: DrawingStroke['tone'] = 'line', step?: number, width?: number, dashed?: boolean): DrawingStroke => ({ d, tone, step, width, dashed });

export const DRAWINGS: Record<DrawingName, { strokes: DrawingStroke[]; labels: Omit<DrawingLabel, 'text'>[]; texts: string[] }> = {
  house: {
    strokes: [
      L('M60 430H740', 'soft', 0),
      L('M210 430V250L400 130L590 250V430', 'line', 1, 2.2),
      L('M180 268L400 116L620 268', 'line', 2, 2.2),
      L('M500 186V136H540V214', 'line', 3),
      L('M360 430V330Q360 318 372 318H428Q440 318 440 330V430', 'accent', 4, 2.2),
      L('M245 290H320V360H245ZM282.5 290V360M245 325H320', 'line', 5),
      L('M480 290H555V360H480ZM517.5 290V360M480 325H555', 'line', 5),
      L('M375 205a25 25 0 1 0 50 0a25 25 0 1 0-50 0M400 180V230M375 205H425', 'line', 6),
      L('M660 430V370M660 380Q620 350 640 310Q630 270 668 262Q700 250 705 292Q730 318 700 350Q690 372 660 370', 'soft', 7),
      L('M210 462H590M210 452V472M590 452V472', 'accent', 8, 1.4, true),
      L('M150 430V130M140 130H160M140 430H160', 'accent', 8, 1.4, true),
    ],
    labels: [
      { x: 400, y: 490, step: 8, anchor: 'middle', tone: 'accent' },
      { x: 136, y: 284, step: 8, anchor: 'end', tone: 'accent' },
      { x: 400, y: 404, step: 4, anchor: 'middle' },
    ],
    texts: ['12.40 m', '7.80 m', 'Entrance'],
  },
  route: {
    strokes: [
      L('M40 120C160 90 240 160 360 130S560 60 760 110', 'soft', 0),
      L('M40 210C150 190 260 250 380 220S580 160 760 200', 'soft', 0),
      L('M40 330C170 300 280 370 420 340S600 290 760 320', 'soft', 0),
      L('M40 430C190 410 300 460 450 440S640 400 760 430', 'soft', 0),
      L('M520 30C500 120 560 180 530 260S470 380 520 490', 'soft', 1, 3),
      L('M110 400C170 380 180 300 250 290S330 340 390 300S420 200 480 190S560 230 610 170S650 110 700 100', 'accent', 2, 3.2),
      L('M110 400m-14 0a14 14 0 1 0 28 0a14 14 0 1 0-28 0', 'accent', 1, 2.4),
      L('M700 100c0-22-16-34-30-34s-30 12-30 34c0 20 30 50 30 50s30-30 30-50ZM670 92m-8 0a8 8 0 1 0 16 0a8 8 0 1 0-16 0', 'line', 3, 2.2),
      L('M250 290m-6 0a6 6 0 1 0 12 0a6 6 0 1 0-12 0M480 190m-6 0a6 6 0 1 0 12 0a6 6 0 1 0-12 0', 'line', 3, 2),
    ],
    labels: [
      { x: 110, y: 444, step: 1, anchor: 'middle' },
      { x: 262, y: 270, step: 3, anchor: 'start' },
      { x: 492, y: 172, step: 3, anchor: 'start' },
      { x: 700, y: 176, step: 3, anchor: 'middle', tone: 'accent' },
    ],
    texts: ['Start', 'Km 4', 'Km 9', 'Summit 1,840 m'],
  },
  growth: {
    strokes: [
      L('M90 60V420H740', 'line', 0, 2),
      L('M90 330H740M90 240H740M90 150H740', 'soft', 0, 1, true),
      L('M90 400C170 390 200 360 260 350S350 330 400 290S480 250 540 210S640 120 720 90', 'accent', 1, 3.2),
      L('M90 400C170 390 200 360 260 350S350 330 400 290S480 250 540 210S640 120 720 90V420H90Z', 'soft', 2, 1),
      L('M260 350m-7 0a7 7 0 1 0 14 0a7 7 0 1 0-14 0', 'accent', 3, 2.4),
      L('M400 290m-7 0a7 7 0 1 0 14 0a7 7 0 1 0-14 0', 'accent', 4, 2.4),
      L('M720 90m-9 0a9 9 0 1 0 18 0a9 9 0 1 0-18 0', 'accent', 5, 2.6),
      L('M260 340V300M400 280V240M720 80V44', 'line', 5, 1.4, true),
    ],
    labels: [
      { x: 260, y: 290, step: 3, anchor: 'middle' },
      { x: 400, y: 230, step: 4, anchor: 'middle' },
      { x: 720, y: 34, step: 5, anchor: 'end', tone: 'accent' },
      { x: 90, y: 446, step: 0, anchor: 'start' },
      { x: 740, y: 446, step: 0, anchor: 'end' },
    ],
    texts: ['New website', 'Online booking', '+212% bookings', 'January', 'December'],
  },
  workspace: {
    strokes: [
      L('M60 400H740M100 400V480M700 400V480', 'line', 0, 2.2),
      L('M250 160Q250 150 260 150H540Q550 150 550 160V320Q550 330 540 330H260Q250 330 250 320Z', 'line', 1, 2.2),
      L('M388 330V372M412 330V372M352 400Q352 372 372 372H428Q448 372 448 400', 'line', 1, 2),
      L('M280 186H520M280 214H446M280 242H492M280 284H352', 'accent', 2, 2.2),
      L('M120 400H184M152 400V306L206 244', 'line', 3, 2),
      L('M192 230L242 256L222 276Z', 'line', 3, 2),
      L('M232 270L300 336', 'accent', 4, 1.4, true),
      L('M596 400V362Q596 352 606 352H636Q646 352 646 362V400M646 366Q668 366 668 381Q668 395 646 393', 'line', 5, 2),
      L('M611 340Q604 326 616 316M631 340Q624 326 636 316', 'soft', 5),
      L('M680 400L688 362H728L736 400', 'line', 6, 2),
      L('M708 362Q698 322 672 312M708 362Q722 320 748 306M708 362Q710 326 716 296', 'soft', 6, 1.8),
    ],
    labels: [{ x: 400, y: 128, step: 2, anchor: 'middle', tone: 'accent' }],
    texts: ['Your desk, your site'],
  },
};

const TONE = {
  line: 'color-mix(in oklab, var(--foreground) 78%, transparent)',
  accent: 'var(--primary)',
  soft: 'color-mix(in oklab, var(--foreground) 24%, transparent)',
};

/**
 *   <ScrollDrawing drawing="house" labels={['12.40 m', '7.80 m', 'Entrance']} />
 *   <ScrollDrawing strokes={[{ d: 'M0 0L100 100' }]} viewBox="0 0 100 100" />
 */
export function ScrollDrawing({ drawing = 'house', strokes, labels, viewBox = '0 0 800 500', start = 0.1, end = 0.55, label, className }: ScrollDrawingProps) {
  const ref = useScrollProgress<HTMLDivElement>({ start, end });
  const built = DRAWINGS[drawing];
  const list = strokes ?? built.strokes;
  const lab: DrawingLabel[] = strokes
    ? ((labels as DrawingLabel[] | undefined) ?? [])
    : built.labels.map((x, i) => ({ ...x, text: (labels as string[] | undefined)?.[i] ?? built.texts[i] }));
  const steps = list.map((s, i) => s.step ?? i);
  const max = Math.max(1, ...steps);
  // each step draws in a window of the progress, the windows overlap a little
  const span = 1 / (max + 1);
  const win = (step: number) => ({ a: round(step * span * 0.92, 4), w: round(span * 1.6, 4) });
  return (
    <div ref={ref} role={label ? 'img' : undefined} aria-label={label} aria-hidden={label ? undefined : true} className={cn('relative w-full', className)}>
      <svg viewBox={strokes ? viewBox : '0 0 800 500'} className="block h-auto w-full overflow-visible">
        {list.map((s, i) => {
          const { a, w } = win(steps[i]);
          return (
            <path
              key={i}
              d={s.d}
              pathLength={1}
              fill={s.tone === 'soft' && /Z\s*$/.test(s.d) ? 'color-mix(in oklab, var(--primary) 7%, transparent)' : 'none'}
              stroke={TONE[s.tone ?? 'line']}
              strokeWidth={s.width ?? 1.8}
              strokeLinecap="round"
              strokeLinejoin="round"
              vectorEffect="non-scaling-stroke"
              style={{
                strokeDasharray: s.dashed ? undefined : '1 1',
                strokeDashoffset: s.dashed ? undefined : `calc(1 - clamp(0, (var(--kit-progress, 1) - ${a}) / ${w}, 1))`,
                opacity: s.dashed || /Z\s*$/.test(s.d) ? `clamp(0, (var(--kit-progress, 1) - ${a}) / ${w} * 1.4, 1)` : undefined,
                ...(s.dashed ? { strokeDasharray: '5 6' } : null),
              }}
            />
          );
        })}
        {lab.map((x, i) => {
          const { a, w } = win(x.step);
          return (
            <text
              key={i}
              x={x.x}
              y={x.y}
              textAnchor={x.anchor ?? 'start'}
              className="text-[15px] font-medium tabular-nums"
              fill={x.tone === 'accent' ? 'var(--primary)' : 'var(--muted-foreground)'}
              style={{
                opacity: `clamp(0, (var(--kit-progress, 1) - ${round(a + w * 0.7, 4)}) * 8, 1)`,
                transform: `translateY(calc((1 - clamp(0, (var(--kit-progress, 1) - ${round(a + w * 0.7, 4)}) * 8, 1)) * 6px))`,
              }}
            >
              {x.text}
            </text>
          );
        })}
      </svg>
    </div>
  );
}

Häufige Fragen

Wie installiere ich Scroll-Zeichnung?

Mit aiw add @ai-website/scroll-drawing. Der Befehl legt die Datei components/ui/scroll-drawing.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind motion. Weitere npm-Pakete braucht sie nicht.

Welche Varianten hat Scroll-Zeichnung?

4 Varianten, Bauplan, Route, Wachstumslinie und Arbeitsplatz.

Kann ein KI-Assistent Scroll-Zeichnung einbauen?

Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Scroll-Zeichnung mit aiw_components und installieren sie mit aiw_add @ai-website/scroll-drawing.

Oft gesucht

Direkt zu

In die Zwischenablage kopiert