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
Variants
Details
What Spot illustrations brings along, read from the registry and the source.
- Category
- Graphics
- Area
- Visuals
- 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
- Studio
- All 5 variants editable
- Build with AI
- Over MCP in Claude, Cursor, Copilot and Codex
Props
The SpotIllustrationProps interface from components/ui/spot-illustrations.tsx, with the comments from the code.
kindrequiredSpotKindsecurityspeedgrowthteamdeliverysupportsearchinboxfilescalendarpaymentssuccessofflineuploadlocationideas
sizeoptionalnumber | stringWidth in pixels or any CSS length, the height follows (5 : 4). Default 160.
animateoptionalbooleanAssemble when it scrolls into view. Default true.
labeloptionalstringAccessible 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
'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.