@serenis/illustrations
v0.29.0
Published
Illustration component library for the Serenis design system. SVG components for branding and empty states.
Keywords
Readme
@serenis/illustrations
Illustration component library for the Serenis design system. SVG components for branding and empty states.
Install
npm install @serenis/illustrationsQuick start
# Build
yarn workspace @serenis/illustrations build
# Dev (watch mode)
yarn workspace @serenis/illustrations dev
# Typecheck
yarn workspace @serenis/illustrations typecheckimport { EmptyCalendar, PathPrimary, Joy, SerenisLogoIcon } from '@serenis/illustrations'
import { type SvgProps } from '@serenis/illustrations'
// Default size (each illustration defines its own default height)
<EmptyCalendar />
// Override dimensions
<PathPrimary height={120} />
// Color variant
<EmptyCalendar color="nutrition-60" />Directory structure
libraries/illustrations/
├── src/
│ ├── components/
│ │ └── Svg/ # Shared <svg> wrapper + test
│ │ ├── index.tsx
│ │ └── index.test.tsx
│ ├── download/ # App store / deep-link assets
│ ├── elements/ # Small reusable SVG pieces
│ ├── flow-loading/ # Multi-step loading illustrations
│ ├── generics/ # Empty states, nutrition, conventions, etc.
│ ├── journaling-activities/ # Activity illustrations
│ ├── journaling-moods/ # Mood/emotion illustrations
│ ├── journaling-scores/ # Score-level illustrations (1–5)
│ ├── logo/ # Serenis and product logos
│ ├── paths/ # Therapy path illustrations
│ ├── videocall/ # Video call state illustrations
│ ├── stories/ # Storybook stories
│ │ ├── Overview.mdx # Documentation page
│ │ ├── Download.stories.tsx # Per-category gallery stories
│ │ ├── Elements.stories.tsx
│ │ ├── FlowLoading.stories.tsx
│ │ ├── Generics.stories.tsx
│ │ ├── JournalingActivities.stories.tsx
│ │ ├── JournalingMoods.stories.tsx
│ │ ├── JournalingScores.stories.tsx
│ │ ├── Logo.stories.tsx
│ │ ├── Paths.stories.tsx
│ │ └── Videocall.stories.tsx
│ └── index.ts # Public API (re-exports everything)
├── dist/ # Built output (gitignored)
├── tsup.config.ts # Bundle config
└── package.jsonArchitecture
The Svg primitive
All illustrations render through a shared Svg wrapper that forwards standard SVGProps<SVGSVGElement> and sets xmlns:
import { type SVGProps } from 'react'
export type SvgProps = SVGProps<SVGSVGElement>
export const Svg = (props: SvgProps) => <svg {...props} xmlns="http://www.w3.org/2000/svg" />Component pattern
Every illustration follows the same structure:
import { COLOR_PRIMARY, COLOR_PRIMARY_10 } from '@serenis/design-tokens'
import { Svg, type SvgProps } from '../components/Svg'
export const PathPrimary = ({ height = 164, ...props }: SvgProps) => (
<Svg fill="none" height={height} viewBox="0 0 164 164" {...props}>
<path d="..." fill={COLOR_PRIMARY_10} />
<path d="..." fill={COLOR_PRIMARY} fillRule="evenodd" />
</Svg>
)Key conventions:
- Named export matching the file name
- Default
heightin the destructured props (varies per illustration) ...propsspread on theSvgwrapper for consumer overrides- Fixed
viewBoxmatching the illustration's native dimensions
Domain folders
Illustrations are organized by product domain. Each folder contains one .tsx file per illustration and an index.ts barrel with explicit named re-exports:
| Folder | Description |
| ------------------------ | ---------------------------------------------------------- |
| download/ | App store and deep-link assets (Android, iOS, OneLink) |
| elements/ | Small reusable SVG pieces (Arrow, BalloonTail, DashedLine) |
| flow-loading/ | Multi-step loading illustrations (steps 1–4 + payment) |
| generics/ | Largest set: empty states, nutrition, conventions, devices |
| journaling-activities/ | Activity icons (yoga, water, sleep, sport, etc.) |
| journaling-moods/ | Mood/emotion illustrations (Joy, Anger, Serenity, etc.) |
| journaling-scores/ | Score-level illustrations (1–5) |
| logo/ | Serenis and product logos |
| paths/ | Therapy path illustrations (Primary, Couples, etc.) |
| videocall/ | Video call state illustrations (Joining, TooEarly, etc.) |
Export chain
- Each folder's
index.tsre-exports its components by name src/index.tsre-exports from all folders plusSvg/SvgPropstsupbundlessrc/index.tsinto ESM + CJS + TypeScript declarations
Type system
import { type SvgProps } from '@serenis/illustrations'| Type | Purpose |
| ---------- | -------------------------------------------------------------------------- |
| SvgProps | SVGProps<SVGSVGElement> — base prop type for all illustration components |
Some illustrations extend SvgProps with custom props (see below).
Color integration
Illustrations use @serenis/design-tokens color constants directly in SVG fills and strokes:
import { COLOR_PRIMARY, COLOR_PRIMARY_40, COLOR_BLACK } from '@serenis/design-tokens'
// Used directly in JSX
<path fill={COLOR_PRIMARY} />
<path stroke={COLOR_PRIMARY_40} strokeWidth=".74" />Unlike icons (which defer color to the Icon wrapper via cssvarColor()), illustration colors are baked into the component. This is because illustrations typically use multiple distinct colors in a single artwork.
Avoid
neutral-*tokens in illustrations. Neutral colors (COLOR_NEUTRAL_*) reverse between light and dark themes, which can completely alter the illustration's appearance. For greys and neutral shades, use theme-independent root palette colors (grey-*) or hardcoded hex values instead. Only useneutral-*as a deliberate exception when the reversal is intentionally desired and the visual result has been verified in both themes.
useId() pattern
When an illustration needs SVG <defs> (gradients, masks, clip paths), React useId() generates collision-safe IDs:
import { useId } from 'react'
import { Svg, type SvgProps } from '../components/Svg'
export const EmptyCalendar = ({ height = 164, ...props }: Props) => {
const prefix = useId()
const id1 = `${prefix}-1`
const id2 = `${prefix}-2`
return (
<Svg fill="none" height={height} viewBox="0 0 164 164" {...props}>
<path fill={`url(#${id1})`} d="..." />
<defs>
<linearGradient id={id1}>...</linearGradient>
</defs>
</Svg>
)
}This prevents ID collisions when multiple instances of the same illustration render on a page.
Parameterized illustrations
Some illustrations accept a color variant prop to adapt to different product contexts:
import { type ColorName } from '@serenis/design-tokens'
type Props = {
color?: Extract<ColorName, 'primary' | 'nutrition-60'>
} & SvgProps
export const EmptyCalendar = ({ color = 'primary', height = 164, ...props }: Props) => {
// Map the variant to a set of internal colors
const COLORS = {
primary: { rings: COLOR_PRIMARY_40, top: COLOR_PRIMARY },
'nutrition-60': { rings: COLOR_NUTRITION_40, top: COLOR_NUTRITION_60 },
}
return (
<Svg ...>
<path fill={COLORS[color].top} />
</Svg>
)
}How to add a new illustration
Create the component file at
src/<domain>/MyIllustration.tsx:import { COLOR_PRIMARY } from '@serenis/design-tokens' import { Svg, type SvgProps } from '../components/Svg' export const MyIllustration = ({ height = 164, ...props }: SvgProps) => ( <Svg fill="none" height={height} viewBox="0 0 164 164" {...props}> {/* Paste SVG paths here, use @serenis/design-tokens constants for colors */} </Svg> )Export from the folder's
index.ts(keep alphabetical order):export { MyIllustration } from './MyIllustration'Export from
src/index.ts(keep alphabetical order):export { MyIllustration } from './<domain>'Build and typecheck:
yarn workspace @serenis/illustrations build
yarn workspace @serenis/illustrations typecheckThe new illustration will automatically appear in the corresponding Storybook gallery story.
Build
The package is bundled with tsup into ESM + CJS + TypeScript declarations. Workspace dependencies (@serenis/design-tokens, react) are externalized.
Consumers
- Apps:
apps/web(primary consumer),apps/blog,apps/nutrition-blog,apps/website - Libraries:
libraries/ui(@serenis/ui)
Documentation
- Storybook: Overview page and per-domain gallery stories in
apps/design-system - AGENTS.md: Agent-oriented API reference and rules — see AGENTS.md
License
CC BY-NC-ND 4.0 — see LICENSE for the full text.
