@serenis/icons
v2.42.0
Published
Icon component library for the Serenis design system. Wraps custom SVG icons and re-exports from iconoir-react.
Keywords
Readme
@serenis/icons
Icon component library for the Serenis design system. Wraps custom SVG icons and re-exports from iconoir-react.
Install
npm install @serenis/iconsPeer dependencies (must be installed by the consuming app):
react>= 19.0.0@serenis/design-tokens
Quick start
# Build
yarn workspace icons build
# Dev (watch mode)
yarn workspace icons dev
# Typecheck
yarn workspace icons typecheckimport { Icon, Home, IconHome } from '@serenis/icons'
import { type IconElement, type IconCommonProps, type IconSize } from '@serenis/icons'
// Option A: Icon wrapper with raw SVG component (dynamic / slotted usage)
<Icon Svg={Home} colorName="primary" size={24} />
// Option B: Pre-wrapped shorthand (fixed icon usage)
<IconHome colorName="primary" size={24} />Directory structure
libraries/icons/
├── src/
│ ├── components/ # One .tsx file per icon + Icon.tsx wrapper
│ │ ├── Icon.tsx # Design-token-aware wrapper
│ │ ├── Home.tsx # Example: exports Home + IconHome
│ │ ├── Agenda.tsx
│ │ └── ... # custom icon files
│ ├── stories/
│ │ ├── Overview.mdx # Storybook documentation page
│ │ ├── Playground.stories.tsx # Storybook playground for all icons
│ │ └── ...
│ ├── types.ts # Shared TypeScript types
│ └── index.ts # Public API (re-exports everything)
├── dist/ # Built output (gitignored)
├── tsup.config.ts # Bundle config
└── package.jsonArchitecture
Two icon sources
The library ships icons from two sources through a single public API:
| Source | Where they live | Icon* shorthand? |
| ------------------ | ----------------------------------- | --------------------------------- |
| Custom Serenis | src/components/*.tsx (inline SVG) | Yes — e.g. Home + IconHome |
| Iconoir | Re-exported from iconoir-react | No — use via <Icon Svg={...} /> |
Custom icons are hand-maintained inline SVG React components designed specifically for the Serenis product. Iconoir icons fill gaps for generic UI glyphs (arrows, navigation chrome, utility actions) that don't need custom artwork.
The Icon wrapper
Icon is the bridge between raw SVG components and the design token system. It accepts any IconElement and applies design-token-based color and size:
const Icon = ({ Svg, colorName, fillColorName, size }: IconProps): JSX.Element | null => {
if (Svg == null) {
return null
}
return (
<Svg
color={colorName ? cssvarColor(colorName) : 'currentColor'}
fill={fillColorName ? cssvarColor(fillColorName) : 'none'}
height={size}
width={size}
/>
)
}colorName— maps tocolor(stroke) viacssvarColor()fromdesign-tokens. Omitting it defaults tocurrentColor, inheriting from the parent text color.fillColorName— maps tofillviacssvarColor(). Omitting it defaults tonone. Rarely used (stars, a few nav icons).size— applied to bothwidthandheight.
Dual export pattern
Every custom icon file exports two things:
// Home.tsx
export const Home = ({ color, height, width }: IconElementProps) => (
<svg ...>...</svg>
)
export const IconHome = (props: IconCommonProps) => <Icon Svg={Home} {...props} />Home— the raw SVG component. Use when passing as a reference to theIconwrapper or to component props typed asIconElement(e.g. Button'sIconStart).IconHome— pre-wrapped shorthand. Use for direct rendering when you don't need to pass the icon as a reference.
Iconoir icons are only exported as raw components (no Icon* shorthand).
Type system
All types are exported from the package root:
import {
type IconElement,
type IconElementProps,
type IconCommonProps,
type IconProps,
type IconSize,
} from '@serenis/icons'| Type | Purpose |
| ------------------ | ------------------------------------------------------------------------------------------- |
| IconSize | 12 \| 16 \| 20 \| 24 \| 32 \| 40 \| 48 \| 56 \| 64 \| 72 \| 80 \| 120 |
| IconElementProps | Low-level contract for SVG components: { color, fill, height, width } |
| IconElement | Any component that accepts IconElementProps — use for props that accept an icon reference |
| IconCommonProps | Token-based API: { colorName?, fillColorName?, size } |
| IconProps | IconCommonProps & { Svg?: IconElement } — the Icon wrapper props |
Size scale
| Size | | ---------------- | | 12 | | 16 | | 20 | | 24 (default) | | 32 | | 40 | | 48 | | 56 | | 64 | | 72 | | 80 | | 120 |
Usage patterns
Direct rendering
// Pre-wrapped shorthand (custom icons only)
<IconHome colorName="primary" size={24} />
// Icon wrapper (both custom and Iconoir)
<Icon Svg={Home} colorName="primary" size={24} />
<Icon Svg={ArrowLeft} colorName="neutral-80" size={24} />Passing icons as props
Many ui components accept IconElement as a prop:
// Button with start icon
<Button IconStart={Home} kind="primary">Go home</Button>
// Tile with required icon
<Tile Icon={Shield} title="Security" action={<Button>Configure</Button>} />
// SideBarItem with active/inactive variants
<SideBarItem Icon={Agenda} IconActive={AgendaSolid} label="Agenda" active={isActive} />Active / inactive pattern
Many custom icons have a Solid variant for active states:
<Icon Svg={active ? AgendaSolid : Agenda} colorName={active ? 'primary-50' : 'neutral-110'} size={24} />Available pairs: Agenda/AgendaSolid, Calendar/CalendarSolid, Chat/ChatSolid, Clock/ClockSolid, Codes/CodesSolid, Delete/DeleteSolid, Diary/DiarySolid, Home/HomeSolid, Menu/MenuSolid, Patients/PatientsSolid, Paths/PathsSolid, and all Path*/Path*Solid therapy path icons.
Aliasing when names clash
Some icon names conflict with common identifiers. Alias on import:
import { Card as IconCard, Link as IconLink } from '@serenis/icons'Inheriting color from parent
Omit colorName to use currentColor — the icon inherits the parent's text color:
<Text colorName="primary">
<Icon Svg={Home} size={24} /> {/* stroke is primary */}
</Text>Accessibility
Icons rendered through the Icon wrapper are decorative by default — they carry no accessible name or role. Meaning is expected to come from adjacent text or from the parent control's label.
Icon with text (no extra ARIA needed)
When an icon appears alongside visible text, the text provides the accessible name. No extra attributes are required on the icon:
<Button IconStart={Home} kind="primary">Go home</Button>
<Flex>
<Icon Svg={Info} size={24} />
<Text>Important information</Text>
</Flex>Icon-only controls need aria-label
When an icon is the sole content of a clickable control, the parent element must provide an accessible name via aria-label or aria-labelledby:
<Button IconStart={Delete} kind="tertiary" aria-label="Delete item" />
<Pressable aria-label="Close" onClick={onClose}>
<Icon Svg={ActionCloseLine} size={24} />
</Pressable>Why the Icon wrapper has no ARIA attributes
The Icon component intentionally does not set aria-hidden, role, or any ARIA attribute. Whether an icon is decorative or meaningful depends on the usage context, which only the caller knows. Callers should:
- Add
aria-labelto the parent control when the icon is the only content - Do nothing when adjacent text already conveys the meaning
- Avoid adding
aria-hidden="true"to icons that appear alongside text (the text already provides the name)
ESLint enforcement
The custom ESLint rule serenis/no-icon-inside-button (active as warn in libraries/ui, apps/web, apps/blog, apps/website, apps/nutrition-blog) prevents nesting Icon or Icon* components as children of Button. Use the IconStart / IconEnd props instead — this ensures consistent sizing, loading-state handling, and accessible markup:
// Correct
<Button IconStart={Home} kind="primary">Go home</Button>
// Incorrect — caught by ESLint
<Button><Icon Svg={Home} size={24} />Go home</Button>How to add a new icon
There is no codegen pipeline — icons are hand-maintained TSX files.
- Create the component file at
src/components/MyIcon.tsx:
import { type IconCommonProps, type IconElementProps } from '../types'
import Icon from './Icon'
type Props = IconElementProps
export const MyIcon = ({ color, height, width }: Props) => (
<svg fill="none" height={height} viewBox="0 0 24 24" width={width} xmlns="http://www.w3.org/2000/svg">
{/* Paste SVG paths here, use color={color} for strokes */}
</svg>
)
export const IconMyIcon = (props: IconCommonProps) => <Icon Svg={MyIcon} {...props} />- Export from
src/index.ts(keep alphabetical order):
export { IconMyIcon, MyIcon } from './components/MyIcon'- Build and typecheck:
yarn workspace icons build
yarn workspace icons typecheckThe new icon will automatically appear in the Storybook Playground story.
Build
The package is bundled with tsup into ESM + CJS + TypeScript declarations. Workspace dependencies (design-tokens, iconoir-react, react) are externalized.
Consumers
- Apps:
apps/web(primary consumer),apps/blog,apps/nutrition-blog,apps/website - Libraries:
libraries/ui(Button, Modal, Accordion, Breadcrumbs, FileUploadField, etc.),libraries/shared-components(Nav)
Documentation
- Storybook: Overview page and interactive Playground in
apps/design-system - AGENTS.md: Agent-oriented API reference and rules — see AGENTS.md
License
Custom Serenis SVG icons: CC BY-NC-ND 4.0 — see LICENSE for the full text. Iconoir re-exports retain their upstream MIT license.
