Skip to content
UI Kit
Accent color

Spot illustrations

Sixteen small drawings in one style for features, empty states, steps and confirmations. Security, speed, growth, team, delivery, support, search, inbox, files, calendar, payments, success, offline, upload, location and ideas. They take the theme colors and assemble as they scroll in.

One of 5 components in Graphics

5 variants

Variants

The set

All sixteen motifs with their names. Each one assembles part by part as it scrolls in, its accent part lifts on hover.

Open in Studio

Feature cards

Three features with a drawing each. The cards rise one after another, hover lifts the card and moves its drawing.

Open in Studio

Empty states

A search without results, an empty inbox and a first upload, each with a drawing, a clear line and the next step. The switch shows them in turn.

Open in Studio

Steps

Three steps with drawings, joined by a line that fills while you scroll. Phones stack them.

Open in Studio

Confirmation

One large drawing above the message of a confirmation page, under a soft light. The drawing assembles first, the rest follows.

Open in Studio

Details

What Spot illustrations brings along, read from the registry and the source.

Category
Graphics
Variants
5
Version
1.0.0 · since 1.1.0
File
components/ui/spot-illustrations.tsx
Brings along
utilskit
npm packages
None
Languages
Texts come through props

Props

The SpotIllustrationProps interface from components/ui/spot-illustrations.tsx, with the comments from the code.

  • kindrequired
    SpotKind

    securityspeedgrowthteamdeliverysupportsearchinboxfilescalendarpaymentssuccessofflineuploadlocationideas

  • sizeoptional
    number | string

    Width in pixels or any CSS length, the height follows (5 : 4). Default 160.

  • animateoptional
    boolean

    Assemble when it scrolls into view. Default true.

  • labeloptional
    string

    Accessible name. Without it the drawing is decoration.

Accessibility

Read from the source of the component.

  • Sets aria-label, so screen readers announce purpose and state.
  • Uses the role label.
  • Needs no client state. The server writes it in full, so it reads without JavaScript too.

Motion

  • With reduced motion (prefers-reduced-motion) it stays still.
  • No animations, it stands still from the start.

Source

The file aiw add puts into your project. From then on it is yours.

Show the file, 236 lines
components/ui/spot-illustrations.tsx
'use client';

import { cn, useInView } from '@/lib/kit';

/*
 * Small spot illustrations for features, empty states and steps. Sixteen
 * motifs in one style: a soft card ground, one accent shape, quiet lines in
 * the text color and a few dots of light. They take the theme colors, so they
 * sit in light and dark mode and follow the accent.
 *
 * Each motif assembles itself once it scrolls into view (the parts rise one
 * after another) and its accent part moves a little on hover of the nearest
 * .group parent. The server renders the finished drawing.
 */

export type SpotKind =
  | 'security'
  | 'speed'
  | 'growth'
  | 'team'
  | 'delivery'
  | 'support'
  | 'search'
  | 'inbox'
  | 'files'
  | 'calendar'
  | 'payments'
  | 'success'
  | 'offline'
  | 'upload'
  | 'location'
  | 'ideas';

export const SPOT_KINDS: SpotKind[] = ['security', 'speed', 'growth', 'team', 'delivery', 'support', 'search', 'inbox', 'files', 'calendar', 'payments', 'success', 'offline', 'upload', 'location', 'ideas'];

export interface SpotIllustrationProps {
  kind: SpotKind;
  /** Width in pixels or any CSS length, the height follows (5 : 4). Default 160. */
  size?: number | string;
  /** Assemble when it scrolls into view. Default true. */
  animate?: boolean;
  /** Accessible name. Without it the drawing is decoration. */
  label?: string;
  className?: string;
}

const A = 'var(--primary)';
const A2 = 'color-mix(in oklab, var(--primary) 30%, var(--card))';
const G = 'color-mix(in oklab, var(--foreground) 6%, var(--card))';
const L = 'color-mix(in oklab, var(--foreground) 16%, var(--card))';
const S = 'color-mix(in oklab, var(--foreground) 55%, transparent)';
const W = 'var(--card)';

const STYLES = `
.ksp-part{transition:opacity .6s var(--ease-kit),transform .7s var(--ease-kit);transition-delay:calc(var(--i,0)*90ms)}
.ksp-off .ksp-part{opacity:0;transform:translateY(8px) scale(.96)}
.ksp-acc{transition:transform .6s var(--ease-kit);transform-box:fill-box;transform-origin:center}
.group:hover .ksp-acc,.ksp-root:hover .ksp-acc{transform:translateY(-3px) rotate(-4deg)}
@media (prefers-reduced-motion:reduce){.ksp-part,.ksp-acc{transition:none}.ksp-off .ksp-part{opacity:1;transform:none}}
`;

/** One part of a motif, with its place in the assembly order */
const P = ({ i, acc, children }: { i: number; acc?: boolean; children: React.ReactNode }) => (
  <g className="ksp-part" style={{ '--i': i } as React.CSSProperties}>
    {acc ? <g className="ksp-acc">{children}</g> : children}
  </g>
);

const stroke = { stroke: S, strokeWidth: 2, strokeLinecap: 'round' as const, strokeLinejoin: 'round' as const, fill: 'none' };

function art(kind: SpotKind): React.ReactNode {
  switch (kind) {
    case 'security':
      return (
        <>
          <P i={0}><rect x="22" y="22" width="76" height="60" rx="10" fill={G} /></P>
          <P i={1} acc><path d="M60 26 80 34v14c0 13-8 22-20 28-12-6-20-15-20-28V34Z" fill={A} /></P>
          <P i={2}><path d="m51 50 6 6 12-13" stroke={W} strokeWidth="3.4" strokeLinecap="round" strokeLinejoin="round" fill="none" /></P>
          <P i={3}><circle cx="96" cy="22" r="3" fill={A2} /><circle cx="22" cy="78" r="2" fill={L} /></P>
        </>
      );
    case 'speed':
      return (
        <>
          <P i={0}><circle cx="60" cy="56" r="30" fill={G} /></P>
          <P i={1}><path d="M38 64a22 22 0 1 1 44 0" {...stroke} /><path d="M42 50h3M60 36v3M78 50h-3" {...stroke} /></P>
          <P i={2} acc><path d="M60 64 74 44" stroke={A} strokeWidth="4" strokeLinecap="round" /><circle cx="60" cy="64" r="5" fill={A} /></P>
          <P i={3}><path d="M12 30h14M8 40h12M14 50h8" stroke={A2} strokeWidth="3" strokeLinecap="round" /></P>
        </>
      );
    case 'growth':
      return (
        <>
          <P i={0}><rect x="18" y="20" width="84" height="64" rx="10" fill={G} /></P>
          <P i={1}>{[0, 1, 2, 3].map((k) => <rect key={k} x={30 + k * 16} y={66 - k * 9} width="9" height={10 + k * 9} rx="2.5" fill={L} />)}</P>
          <P i={2} acc><path d="M28 60 46 48l12 6 22-20" stroke={A} strokeWidth="3.4" strokeLinecap="round" strokeLinejoin="round" fill="none" /><path d="M72 32h9v9" stroke={A} strokeWidth="3.4" strokeLinecap="round" strokeLinejoin="round" fill="none" /></P>
        </>
      );
    case 'team':
      return (
        <>
          <P i={0}><rect x="16" y="54" width="88" height="28" rx="14" fill={G} /></P>
          <P i={1}><circle cx="38" cy="44" r="10" fill={L} /><path d="M24 74c2-9 8-14 14-14s12 5 14 14" fill={L} /></P>
          <P i={2}><circle cx="82" cy="44" r="10" fill={L} /><path d="M68 74c2-9 8-14 14-14s12 5 14 14" fill={L} /></P>
          <P i={3} acc><circle cx="60" cy="38" r="12" fill={A} /><path d="M42 74c2-12 9-18 18-18s16 6 18 18" fill={A} /></P>
        </>
      );
    case 'delivery':
      return (
        <>
          <P i={0}><path d="M10 78h100" {...stroke} /></P>
          <P i={1} acc><rect x="22" y="36" width="46" height="34" rx="4" fill={A} /><path d="M68 46h16l10 12v12H68Z" fill={A2} /></P>
          <P i={2}><circle cx="38" cy="74" r="7" fill={W} stroke={S} strokeWidth="3" /><circle cx="82" cy="74" r="7" fill={W} stroke={S} strokeWidth="3" /></P>
          <P i={3}><path d="M6 44h10M2 54h12" stroke={A2} strokeWidth="3" strokeLinecap="round" /></P>
        </>
      );
    case 'support':
      return (
        <>
          <P i={0}><rect x="16" y="22" width="56" height="36" rx="12" fill={G} /><path d="M30 58v10l10-10" fill={G} /></P>
          <P i={1}><path d="M28 36h32M28 45h20" {...stroke} /></P>
          <P i={2} acc><rect x="52" y="44" width="52" height="32" rx="12" fill={A} /><path d="M92 76v9l-9-9" fill={A} /></P>
          <P i={3}><circle cx="66" cy="60" r="2.6" fill={W} /><circle cx="78" cy="60" r="2.6" fill={W} /><circle cx="90" cy="60" r="2.6" fill={W} /></P>
        </>
      );
    case 'search':
      return (
        <>
          <P i={0}><rect x="16" y="20" width="70" height="56" rx="10" fill={G} /></P>
          <P i={1}><path d="M28 34h34M28 44h22M28 54h28" {...stroke} /></P>
          <P i={2} acc><circle cx="74" cy="56" r="15" fill={W} stroke={A} strokeWidth="4" /><path d="m85 67 13 13" stroke={A} strokeWidth="5" strokeLinecap="round" /></P>
        </>
      );
    case 'inbox':
      return (
        <>
          <P i={0}><path d="M24 50 34 26h52l10 24v26H24Z" fill={G} /></P>
          <P i={1}><path d="M24 50h22l4 8h20l4-8h22" {...stroke} /></P>
          <P i={2} acc><rect x="42" y="14" width="36" height="26" rx="4" fill={A} /><path d="m44 18 16 12 16-12" stroke={W} strokeWidth="2.6" fill="none" strokeLinejoin="round" /></P>
          <P i={3}><circle cx="96" cy="20" r="3" fill={A2} /></P>
        </>
      );
    case 'files':
      return (
        <>
          <P i={0}><rect x="20" y="30" width="44" height="54" rx="6" fill={L} transform="rotate(-8 42 57)" /></P>
          <P i={1}><path d="M46 18h28l14 14v50H46Z" fill={G} /><path d="M74 18v14h14" fill={L} /></P>
          <P i={2}><path d="M54 46h26M54 55h26M54 64h16" {...stroke} /></P>
          <P i={3} acc><circle cx="88" cy="76" r="11" fill={A} /><path d="m83 76 4 4 7-8" stroke={W} strokeWidth="2.8" fill="none" strokeLinecap="round" strokeLinejoin="round" /></P>
        </>
      );
    case 'calendar':
      return (
        <>
          <P i={0}><rect x="22" y="22" width="76" height="62" rx="10" fill={G} /><rect x="22" y="22" width="76" height="16" rx="8" fill={L} /></P>
          <P i={1}><path d="M38 16v12M82 16v12" {...stroke} /></P>
          <P i={2}>{Array.from({ length: 8 }, (_, k) => <rect key={k} x={32 + (k % 4) * 15} y={46 + Math.floor(k / 4) * 15} width="9" height="9" rx="2.5" fill={L} />)}</P>
          <P i={3} acc><rect x="62" y="61" width="11" height="11" rx="3" fill={A} /></P>
        </>
      );
    case 'payments':
      return (
        <>
          <P i={0}><rect x="14" y="30" width="70" height="44" rx="8" fill={G} /><path d="M14 42h70" stroke={L} strokeWidth="7" /></P>
          <P i={1} acc><rect x="36" y="22" width="70" height="44" rx="8" fill={A} /><path d="M36 34h70" stroke="color-mix(in oklab, var(--primary) 70%, black)" strokeWidth="7" /></P>
          <P i={2}><rect x="46" y="48" width="18" height="8" rx="2" fill={W} opacity="0.85" /><circle cx="90" cy="54" r="5" fill={W} opacity="0.6" /><circle cx="96" cy="54" r="5" fill={W} opacity="0.85" /></P>
        </>
      );
    case 'success':
      return (
        <>
          <P i={0}><circle cx="60" cy="52" r="32" fill={G} /></P>
          <P i={1} acc><circle cx="60" cy="52" r="20" fill={A} /><path d="m51 52 6 6 12-13" stroke={W} strokeWidth="3.6" fill="none" strokeLinecap="round" strokeLinejoin="round" /></P>
          <P i={2}><path d="M22 22l4 4M98 22l-4 4M16 60h6M98 60h6M30 90l3-4M90 90l-3-4" stroke={A2} strokeWidth="3" strokeLinecap="round" /></P>
        </>
      );
    case 'offline':
      return (
        <>
          <P i={0}><circle cx="60" cy="52" r="34" fill={G} /></P>
          <P i={1}><path d="M34 46a38 38 0 0 1 52 0M42 55a26 26 0 0 1 36 0" {...stroke} /><circle cx="60" cy="66" r="4" fill={S} /></P>
          <P i={2} acc><path d="M32 26 88 82" stroke={A} strokeWidth="4.4" strokeLinecap="round" /></P>
        </>
      );
    case 'upload':
      return (
        <>
          <P i={0}><path d="M32 74a18 18 0 0 1 2-36 24 24 0 0 1 46-4 20 20 0 0 1 8 40Z" fill={G} /></P>
          <P i={1} acc><path d="M60 72V44" stroke={A} strokeWidth="4.4" strokeLinecap="round" /><path d="m48 54 12-12 12 12" stroke={A} strokeWidth="4.4" fill="none" strokeLinecap="round" strokeLinejoin="round" /></P>
          <P i={2}><circle cx="96" cy="24" r="3" fill={A2} /><circle cx="20" cy="30" r="2" fill={L} /></P>
        </>
      );
    case 'location':
      return (
        <>
          <P i={0}><path d="M14 74 40 64l40 10 26-10v14l-26 10-40-10-26 10Z" fill={G} /></P>
          <P i={1}><path d="M40 64v14M80 74v14" stroke={L} strokeWidth="2" /></P>
          <P i={2} acc><path d="M60 70c-12-14-18-23-18-31a18 18 0 0 1 36 0c0 8-6 17-18 31Z" fill={A} /><circle cx="60" cy="39" r="6.5" fill={W} /></P>
        </>
      );
    default:
      return (
        <>
          <P i={0}><circle cx="60" cy="44" r="30" fill={G} /></P>
          <P i={1} acc><path d="M60 22a18 18 0 0 0-10 33v7h20v-7a18 18 0 0 0-10-33Z" fill={A} /><rect x="51" y="66" width="18" height="6" rx="3" fill={A2} /></P>
          <P i={2}><path d="M60 8v6M32 20l4 4M88 20l-4 4M24 44h6M90 44h6" stroke={A2} strokeWidth="3" strokeLinecap="round" /></P>
        </>
      );
  }
}

/**
 * A small drawing for a feature, an empty state or a step.
 *   <SpotIllustration kind="security" size={140} />
 */
export function SpotIllustration({ kind, size = 160, animate = true, label, className }: SpotIllustrationProps) {
  const [ref, inView] = useInView<SVGSVGElement>('0px 0px -10% 0px');
  return (
    <>
      <style href="kit-spot-illustrations" precedence="default">
        {STYLES}
      </style>
      <svg
        ref={ref}
        viewBox="0 0 120 96"
        role={label ? 'img' : undefined}
        aria-label={label}
        aria-hidden={label ? undefined : true}
        className={cn('ksp-root h-auto shrink-0 overflow-visible', animate && !inView && 'ksp-off', className)}
        style={{ width: size }}
      >
        {art(kind)}
      </svg>
    </>
  );
}

Frequent questions

How do I install Spot illustrations?

With aiw add @ai-website/spot-illustrations. The command puts components/ui/spot-illustrations.tsx into your project, the code is yours from then on. It needs no further npm packages.

Which variants does Spot illustrations have?

5 variants, The set, Feature cards, Empty states, Steps and Confirmation.

Can an AI assistant build with Spot illustrations?

Yes. Through the MCP server of the aiw CLI, Claude, Cursor, GitHub Copilot, Windsurf, Codex and Gemini find Spot illustrations with aiw_components and install it with aiw_add @ai-website/spot-illustrations.

Can I adjust Spot illustrations without code?

Yes, in the Studio. All 5 variants are editable there, texts, lists and switches. The Studio writes the matching SpotIllustration code to copy.

Often searched

Jump to

Copied to the clipboard