| name | illustration-system |
| description | Scalable, theme-aware illustrations for mobile — pipeline, tokenization, and platform delivery. Use this when authoring or shipping illustrations. |
Illustration System
Instructions
Illustrations are large, expressive assets. Done right they feel native in both light and dark mode, scale without blur, and ship small. Done wrong they bloat the binary and break on dark backgrounds.
1. Vector-First
Ship SVG source; raster only when there is no alternative (photographic content, complex gradients that fail to render efficiently as vectors).
- Android: VectorDrawable for simple work; rastered WebP (lossless) for complex.
- iOS: PDF / SVG in asset catalog; multi-resolution rasters only for photographic content.
- Flutter:
flutter_svg for vector; built-in Image.asset for raster (provide 1×/2×/3×).
- React Native:
react-native-svg components; or @react-native-assets/slider-style 1×/2×/3× raster sets.
2. Theme-Aware Authoring
Illustrations must work in light and dark mode. Two strategies:
- Token-driven single source (preferred): author with CSS custom properties or
currentColor placeholders that map to semantic tokens at render time.
- Paired sources: ship
empty-inbox.light.svg and empty-inbox.dark.svg only when the visual language differs meaningfully.
Token-driven example:
<svg viewBox="0 0 240 160" xmlns="http://www.w3.org/2000/svg">
<rect width="240" height="160" rx="16" fill="var(--ds-surface-subtle, currentColor)"/>
<path d="..." fill="var(--ds-content-muted)"/>
<path d="..." fill="var(--ds-action-primary-bg)"/>
</svg>
The build strips CSS variables and substitutes a token-typed widget per platform.
3. Tokenized Illustration Slots
Define a small alias set used by every illustration:
{
"illustration": {
"fg": { "$value": "{color.content.default}" },
"fgMuted": { "$value": "{color.content.muted}" },
"accent": { "$value": "{color.action.primary.bg}" },
"surface": { "$value": "{color.surface.subtle}" }
}
}
4. Compose Delivery
@Composable
fun Illustration(id: IllustrationId, modifier: Modifier = Modifier) {
val colors = DsTheme.illustration
val painter = when (id) {
IllustrationId.EmptyInbox -> rememberEmptyInboxPainter(colors)
}
Image(painter = painter, contentDescription = null, modifier = modifier)
}
rememberEmptyInboxPainter is a generated Compose Painter from the SVG with colors wired to the theme.
5. SwiftUI Delivery
Two paths:
- Compile SVGs to PDF and drop into
.xcassets with "Preserve Vector Data" enabled; use symbol image tinting for single-color illustrations.
- For multi-color illustrations, generate
Shape / Path code via svg-to-swiftui and inject colors from the environment.
struct EmptyInbox: View {
@Environment(\.dsTheme) var theme
var body: some View {
ZStack {
BackgroundShape().fill(theme.illustration.surface)
ForegroundShape().fill(theme.illustration.fg)
AccentShape().fill(theme.illustration.accent)
}
.accessibilityHidden(true)
}
}
6. Flutter Delivery
class DsIllustration extends StatelessWidget {
const DsIllustration.emptyInbox({super.key, this.size = 240});
final double size;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context).extension<AppIllustration>()!;
return SvgPicture.asset(
'assets/illustrations/empty_inbox.svg',
width: size,
colorFilter: null, // use multi-color token substitution via build
theme: SvgTheme(currentColor: theme.fg),
);
}
}
7. React Native Delivery
export const EmptyInbox = ({ width = 240 }: { width?: number }) => {
const t = useTheme().illustration;
return (
<Svg width={width} viewBox="0 0 240 160">
<Rect width={240} height={160} rx={16} fill={t.surface} />
<Path d="..." fill={t.fgMuted} />
<Path d="..." fill={t.accent} />
</Svg>
);
};
8. Sizing & Layout
Illustrations are aspect-ratio constrained. Components accept a width or maxWidth and preserve intrinsic ratio; never distort by fixing both axes. Provide empty-state illustrations at 240, 320, and 400 widths as tokens, not per screen.
9. Accessibility
Illustrations are decorative by default:
- Compose:
contentDescription = null.
- SwiftUI:
.accessibilityHidden(true).
- Flutter:
ExcludeSemantics or Semantics(label: '').
- RN:
accessibilityElementsHidden + importantForAccessibility="no-hide-descendants".
When an illustration carries informational meaning, provide a real label.
10. Performance
- Keep vector node count sane (< ~300). If an illustration has thousands of paths, ship WebP/PDF raster.
- Cache painters/drawables at screen lifecycle, not per-frame.
- On Android, test on a mid-tier device; VectorDrawables can be expensive.
11. Anti-Patterns
- Full-color hex embedded in SVG source.
- Separate JPEG for every illustration (large binaries, blur on scale).
- Splash screen illustrations that are not vector or not asset-catalog-backed.
- Illustrations without a dark-mode story.
- Fixed-pixel
width/height attributes in the SVG source.
Checklist