@aircall/ds
v0.6.0
Published
Aircall Design System - Modern UI Component Library
Readme
@aircall/ds - Aircall Design System
Modern React component library built with TailwindCSS 4, Radix UI primitives, and TypeScript.
Tech Stack
- React 19 - Latest React with compiler optimizations
- TailwindCSS 4 - Utility-first CSS with OKLch color space
- Radix UI - Unstyled, accessible component primitives
- TypeScript 5.7 - Type-safe development
- Class Variance Authority (CVA) - Variant-based component styling
- Storybook 10 - Interactive component documentation
Installation
For Monorepo Apps (Hydra Workspace)
If you're developing within the Hydra monorepo (e.g., apps/aw-web, apps/hubspot-cti):
# In your app's package.json
{
"dependencies": {
"@aircall/ds": "workspace:*"
}
}For External Apps
If you're using the design system in an external project or consuming the published NPM package:
pnpm add @aircall/dsRequired peer dependencies:
pnpm add react react-dom @aircall/icons @aircall/numbersOptional peer dependencies (install only if you use these components):
| Component | Packages to install |
|-----------|-------------------|
| Calendar | react-day-picker, date-fns |
| DataTable | @tanstack/react-table |
# If using Calendar
pnpm add react-day-picker date-fns
# If using DataTable
pnpm add @tanstack/react-tableUsage
Import Components
Components work the same way in both monorepo and external apps:
import { Button } from '@aircall/ds';
function App() {
return (
<Button variant="default" color="primary" size="default">
Click me
</Button>
);
}Import CSS Styles
The CSS import differs depending on your environment:
Monorepo Apps
In monorepo apps, import the source CSS for optimal build integration:
/* In your app's entry point or globals.css */
import '@aircall/ds/globals.css';
/* Scan your own source files for Tailwind classes */
@source "your_own_source_file";Benefits:
- Source-level integration with your app's Tailwind build
- Shared Tailwind context for better optimization
- Access to theme variables and utilities
External Apps
In external apps, use the pre-compiled CSS bundle:
@import 'tailwindcss';
@import '@aircall/ds/globals.css';
/* Scan your own source files for Tailwind classes */
@source "your_own_source_file";Or in TypeScript/JavaScript entry point:
// main.tsx
import '@aircall/ds/styles.css';Benefits:
- No need to configure Tailwind to scan node_modules
- Smaller bundle size (pre-compiled and minified)
- Faster build times
- Simpler setup
Dark Mode
Dark mode is controlled via the data-theme attribute:
// Light mode (default)
<html>
// Dark mode
<html data-theme="dark">The design system uses CSS variables that automatically adjust based on this attribute.
Components
Button
Versatile button component with multiple variants, colors, sizes, and shapes.
Props:
variant: 'default' | 'outline' | 'ghost' | 'link'color: 'primary' | 'secondary' | 'brand-primary' | 'brand-secondary' | 'brand-destructive' | 'info' | 'success' | 'warning' | 'destructive'size: 'xs' | 'sm' | 'default' | 'lg'shape: 'default' | 'square' | 'circle'readOnly: booleanblock: boolean (full width)render: ReactElement — render Button as a different element (e.g.<a>). Replaces the formerasChild/ Slot pattern.
Examples:
// Primary button
<Button variant="default" color="primary">Primary</Button>
// Outline button
<Button variant="outline" color="secondary">Outline</Button>
// Icon button
<Button shape="circle" size="sm">
<Phone className="size-4" />
</Button>
// Button with icon
<Button>
<Mail className="size-4" />
Send Email
</Button>
// Full width button
<Button block>Full Width</Button>
// Destructive button
<Button color="destructive">Delete</Button>
// Link rendered as a button — use the `render` prop
<Button render={<a href="/profile" />}>Go to Profile</Button>Storybook
Interactive component documentation with live examples, controls, and accessibility testing.
Quick Start
cd packages/ds
pnpm sb:devView at: http://localhost:6008
Features
- 🎨 Interactive component playground with live editing
- 🌓 Light/dark mode testing via toolbar switcher
- ♿ Accessibility testing with a11y addon
- 📱 Responsive viewport testing
- 🎭 Pseudo-state simulation (hover, focus, active)
- 📐 Figma design integration
- 📚 Auto-generated documentation from TypeScript types
Building Storybook
pnpm sb:build # plain build — no recipe registry
pnpm sb:build:deploy # full build including the recipe registry (what CI deploys)Static site output: storybook-static/. See Recipes (shadcn Registry) below for when to use each.
Development
Adding Components
This package uses shadcn/ui patterns for component development:
# Add a new component from shadcn
pnpm add <component-name>File Structure
src/
├── components/ # React components (published to npm)
│ └── button/
│ ├── __stories__/
│ │ └── Button.stories.tsx
│ └── button.tsx
├── styles/ # Global styles
│ ├── globals.css # TailwindCSS + theme variables
│ └── brand.css # Aircall brand colors
├── lib/ # Utilities
│ └── utils.ts # cn() helper for class merging
├── hooks/ # Custom React hooks
└── fonts/ # Fellix Aircall font files
├── recipes/ # shadcn registry recipes (copy-paste patterns)
│ ├── combobox-searchable-select.tsx
│ ├── combobox-dropdown-search.tsx
│ └── combobox-multi-select.tsxStyling System
Components use Class Variance Authority (CVA) for variant-based styling:
const buttonVariants = cva('base-classes', {
variants: {
variant: {
default: 'variant-specific-classes',
outline: 'outline-specific-classes'
}
},
defaultVariants: {
variant: 'default'
}
});This provides:
- Type-safe variant props
- Automatic TypeScript inference
- Composable class generation
- TailwindCSS class merging via
tailwind-merge
Theme System
The design system uses CSS variables with OKLch color space for better color perception:
/* Light mode (default) */
:root {
--primary: oklch(52.42% 0.201 192.01);
}
/* Dark mode */
[data-theme='dark'] {
--primary: oklch(78.09% 0.128 192.01);
}Colors automatically adapt when data-theme="dark" is set on the root element.
Recipes (shadcn Registry)
Recipes are pre-built, copy-paste component patterns built on top of @aircall/ds primitives. They are distributed via the shadcn registry — consumers run shadcn add to install a recipe into their project, and they own the code afterwards. They can customize it freely.
Note: in the shadcn registry schema these items are typed
registry:block— that's a shadcn enum value, not our terminology. Internally we call them "recipes".
What is the Registry?
The registry is a set of JSON files that describe each recipe — its name, description, npm dependencies, and full source code. These JSON files are generated by shadcn build and served as static files alongside the DS Storybook.
How it works:
- Recipe source files live in
src/recipes/using relative imports (../components/combobox) pnpm run registry:buildcopies the recipes, rewrites imports to@aircall/ds, runsshadcn build, and outputs JSON files topublic/r/- Storybook serves
public/as static files viastaticDirsconfig - A consumer runs
shadcn add <url>— the CLI fetches the JSON, extracts the source code, and writes it into their project with@aircall/dsimports
Local Development
Run pnpm run registry:build before starting Storybook to generate the JSON files. Then start Storybook:
pnpm run registry:build
pnpm run sb:devThe recipe JSON files are served at http://localhost:6008/r/<name>.json. You can test the full consumer flow locally:
pnpm dlx shadcn@latest add http://localhost:6008/r/combobox-searchable-select.jsonProduction (Published Storybook)
When the DS Storybook is deployed to ds.aircall.io, the same JSON files are served at the public URL. Consumers install recipes with:
pnpm dlx shadcn@latest add https://ds.aircall.io/r/combobox-searchable-select.jsonThe deployment CI job runs pnpm sb:build:ds at the repo root, which maps to sb:build:deploy inside this package. That script runs registry:build first, then storybook build — Storybook's staticDirs: ['../public'] config picks up public/r/ and copies it into storybook-static/r/ so the JSON files ship alongside the deployed Storybook. sb:build on its own does NOT bundle the registry — use sb:build:deploy (or pnpm sb:build:ds at the root) whenever you need the registry included.
Available Recipes
| Recipe | Description |
|---|---|
| combobox-searchable-select | Single-select combobox — input acts as trigger and search filter |
| combobox-dropdown-search | Button-triggered combobox with search inside dropdown |
| combobox-multi-select | Multi-select with inline chips and type-ahead filtering |
Adding a New Recipe
- Create
src/recipes/<name>.tsx— import primitives from../components/ - Add an entry to
registry.jsonat the package root - Add a story in
src/stories/that imports from the recipe - Run
pnpm run registry:buildto generate JSON inpublic/r/ - Verify at
http://localhost:6008/r/<name>.jsonafter starting Storybook
Scripts
# Development
pnpm sb:dev # Start Storybook development server
pnpm lint # Run ESLint
# Building for Distribution
pnpm build:package # Build both JS and CSS for publishing
pnpm build:js # Build JavaScript bundle only
pnpm build:css # Build pre-compiled CSS only
# Registry
pnpm registry:build # Generate recipe JSON files in public/r/
# (rewrites relative imports to @aircall/ds)
# Storybook — three distinct builds
pnpm sb:build # Plain Storybook build. NO registry JSON.
pnpm sb:build:chromatic # Storybook build for Chromatic (--test --stats-json). NO registry JSON.
pnpm sb:build:deploy # Full deployment build: runs registry:build, builds Storybook,
# and copies public/r into storybook-static/r/.
# This is what CI runs (via `pnpm sb:build:ds` at the repo root).
# Testing Package Locally
pnpm pack # Create tarball for local testing
# Component Management
pnpm add <component> # Add new shadcn component
# Maintenance
pnpm clean # Remove build artifacts