Hydra UI Library Conventions
How to build components the way @aircall/ds and @aircall/blocks already do it.
These are the rules the TypeScript types can't express. This skill targets the Aircall
Hydra monorepo — all repo paths below are relative to the Hydra repo root (e.g.
~/.../gitlab/hydra/), not to this skill's directory. The governing principle across both
packages: grep a sibling component before deciding — almost every "should this be A or
B?" already has an established answer next door, and any divergence needs an explicit
written rationale.
Authoritative sources (paths relative to the Hydra monorepo root):
<hydra>/packages/ds/AGENTS.md — DS rules (flat files, Base UI, no barrel files)
<hydra>/packages/blocks/AGENTS.md — blocks rules (compose ds, useRender mandatory).
⚠️ Stale on layout: it says per-component dirs + __tests__/, but the real convention is
flat kebab-case files + stories-as-tests — trust structure-files-and-exports /
testing-stories-are-tests below over AGENTS.md.
<hydra>/.agents/skills/build-ds-component/SKILL.md — the spec-first build workflow
<hydra>/.agents/skills/write-ds-story/SKILL.md — Storybook conventions
When to Apply
- Adding a new component to
@aircall/ds or a new block to @aircall/blocks
- Refactoring an existing component's API
- Deciding between a compound component, a variant prop, or a named slot prop
- Reviewing a UI PR in
packages/ds / packages/blocks
- Wiring styling (CVA +
cn), polymorphism (render / useRender), or exports
Tech stack (both packages)
React 19 · TailwindCSS 4 (OKLch tokens) · Base UI (@base-ui/react) ·
Class Variance Authority (CVA) · tailwind-merge via cn() · TypeScript 5.7 ·
Storybook 10. Icons always from @aircall/react-icons, never lucide-react.
Rule Categories by Priority
| Priority |
Category |
Impact |
Prefix |
| 1 |
Component Architecture |
CRITICAL |
architecture- |
| 2 |
Styling System |
HIGH |
styling- |
| 3 |
Component API |
HIGH |
api- |
| 4 |
File & Export Layout |
MEDIUM |
structure- |
| 5 |
Build Workflow |
MEDIUM |
workflow- |
| 6 |
Blocks specifics |
MEDIUM |
blocks- |
| 7 |
Debugging |
MEDIUM |
debug- |
| 8 |
Testing |
HIGH |
testing- |
| 9 |
Shipping |
MEDIUM |
ship- |
Quick Reference
1. Component Architecture (CRITICAL)
architecture-compound-by-default — Multi-part components split into flat-exported
sub-components, each a data-slot; the consumer assembles them (Dialog, Card).
architecture-leaf-uses-variants — A structureless leaf (button, badge, spinner) stays
a single component with CVA variant/size props; do not decompose it.
architecture-named-slot-props — A fixed-position addon (trailing icon, action) is an
explicit named prop, not a child. Avoids React.Children inspection.
2. Styling System (HIGH)
styling-cva-variants — Express variants with cva() + VariantProps, with
defaultVariants. Export the variants object alongside the component.
styling-cn-and-data-slot — Merge classes with cn() from @aircall/ds; set a
data-slot on every element; drive state styling with data-* attributes.
3. Component API (HIGH)
api-children-and-icons — Content (incl. icons) flows through children, not props.
Icons imported from @aircall/react-icons.
api-polymorphism-render — Polymorphism via the Base UI render prop (ds) or
useRender + mergeProps (blocks). Never asChild.
api-typescript-props — forwardRef + displayName for interactive components;
props extend the Base UI primitive's props; variants typed via VariantProps.
api-data-driven-collections — A component rendering options from data takes opaque
items + accessor fns (getItemValue/getItemLabel/renderItem, mirror DataCombobox),
string-key values, behavioral flags as *Keys: Set<string> props — never a fixed
{ value, label, icon, disabled } item shape. Always pair with a compound escape hatch.
api-no-typographic-props — Typography (font-size/weight/line-height) is a consumer
text-* className, not a prop; no DS-wide type-scale system exists yet. A CVA size is for
control dimensions/density only, never a type scale.
api-design-checklist — Settle the API before writing code: the recurring forks to
resolve up front (input domain/units, null & non-finite handling, locale/formatting,
escape-hatch typing, extensibility hook, token mapping) so they don't surface mid-build.
4. File & Export Layout (MEDIUM)
structure-files-and-exports — Flat kebab-case files in src/components/, stories in
src/stories/ (PascalCase). Public API is the top-level src/index.ts barrel only.
No barrel files inside component subdirectories.
5. Build Workflow (MEDIUM)
workflow-spec-first — Non-trivial new component (ds or blocks): investigate first
(Figma + real call-sites + sibling components + the primitive you compose), propose 2-3
candidate APIs with call-site sketches + a trade-off table + a recommendation, and converge
before writing code, then implement + self-verify. Full specs/<name>.spec.md via
/build-ds-component for ds primitives; a short in-conversation API proposal for blocks.
6. Blocks specifics (MEDIUM)
blocks-compose-ds — Import @aircall/ds components and cn directly; never
re-implement a primitive. Interactive/renderable blocks must use useRender +
mergeProps, matching ds's Base UI patterns.
7. Debugging (MEDIUM)
debug-blocks — See a block/component render live: start its Storybook from the Hydra
root (pnpm sb:dev:blocks → :6009, pnpm sb:dev:ds → :6008), then drive it with the
agent-browser-storybook-dev skill — open the preview iframe to render only the
component (no sidebar/toolbar), read console/errors, screenshot, iterate via HMR.
8. Testing (HIGH)
testing-stories-are-tests — There are no *.test.tsx files — stories ARE the tests.
@storybook/addon-vitest runs every story in a real browser; play() = interaction tests.
a11y runs on every story but is report-only by default (test: 'todo') — opt a clean
component in with a11y: { test: 'error' } on its meta to actually gate. Run
pnpm --filter @aircall/ds run sb:test -- --run (CI runs it). Visual regression is Chromatic.
9. Shipping (MEDIUM)
ship-changeset — ds/blocks are published packages; any behavior/API/style change
needs a changeset (pnpm changeset from the Hydra root, or the add-changeset skill).
Pre-1.0 0.x: minor for new component/prop, patch for fixes.
How to Use
Read individual rule files for explanations and incorrect/correct examples:
rules/architecture-compound-by-default.md
rules/styling-cva-variants.md
Related skills
These Aircall conventions are a concrete dialect of two general skills — consult them
for the deeper "why", and read the divergence notes below before applying them verbatim.
vercel-composition-patterns — the underlying composition philosophy. Aircall's
architecture-compound-by-default, architecture-leaf-uses-variants (≈
patterns-explicit-variants), and api-children-and-icons (≈
patterns-children-over-render-props) are direct applications.
⚠️ Two divergences:
- That skill's
react19-no-forwardref says drop forwardRef on React 19.
@aircall/ds is on React 19 but still uses forwardRef + displayName as its
established convention (see button.tsx). Follow the codebase, not the rule.
- Its
patterns-children-over-render-props warns against render-callback props
(renderItem={() => …}). The Base UI render prop Aircall uses is
element-based polymorphism (render={<a />}), a different thing — not in conflict.
vercel-react-best-practices — apply when the component has a performance surface
(lists, expensive renders, heavy deps). Note its bundle-barrel-imports rule is the
same reasoning behind Aircall's "no barrel files in subdirectories" rule
(structure-files-and-exports).
1---2name: aircall-hydra-ui-lib3description: Conventions for building components in Aircall's UI libraries — @aircall/ds (design system, Tailwind 4 + Base UI) and @aircall/blocks (higher-level compositions) — as they live in the Aircall Hydra monorepo. Use when adding or refactoring a component in packages/ds or packages/blocks, deciding between a compound component and a prop, wiring variants, polymorphism, or exports. Triggers on "build a DS component", "add a block", or work touching packages/ds / packages/blocks inside Hydra.4license: MIT5---67# Hydra UI Library Conventions89How to build components the way `@aircall/ds` and `@aircall/blocks` already do it.10These are the rules the TypeScript types can't express. **This skill targets the Aircall11Hydra monorepo** — all repo paths below are relative to the Hydra repo root (e.g.12`~/.../gitlab/hydra/`), not to this skill's directory. The governing principle across both13packages: **grep a sibling component before deciding** — almost every "should this be A or14B?" already has an established answer next door, and any divergence needs an explicit15written rationale.1617Authoritative sources (paths relative to the Hydra monorepo root):1819- `<hydra>/packages/ds/AGENTS.md` — DS rules (flat files, Base UI, no barrel files)20- `<hydra>/packages/blocks/AGENTS.md` — blocks rules (compose ds, `useRender` mandatory).21 ⚠️ Stale on layout: it says per-component dirs + `__tests__/`, but the real convention is22 flat kebab-case files + stories-as-tests — trust `structure-files-and-exports` /23 `testing-stories-are-tests` below over AGENTS.md.24- `<hydra>/.agents/skills/build-ds-component/SKILL.md` — the spec-first build workflow25- `<hydra>/.agents/skills/write-ds-story/SKILL.md` — Storybook conventions2627## When to Apply2829- Adding a new component to `@aircall/ds` or a new block to `@aircall/blocks`30- Refactoring an existing component's API31- Deciding between a compound component, a variant prop, or a named slot prop32- Reviewing a UI PR in `packages/ds` / `packages/blocks`33- Wiring styling (CVA + `cn`), polymorphism (`render` / `useRender`), or exports3435## Tech stack (both packages)3637React 19 · TailwindCSS 4 (OKLch tokens) · Base UI (`@base-ui/react`) ·38Class Variance Authority (CVA) · `tailwind-merge` via `cn()` · TypeScript 5.7 ·39Storybook 10. Icons always from `@aircall/react-icons`, never `lucide-react`.4041## Rule Categories by Priority4243| Priority | Category | Impact | Prefix |44| -------- | ---------------------- | -------- | --------------- |45| 1 | Component Architecture | CRITICAL | `architecture-` |46| 2 | Styling System | HIGH | `styling-` |47| 3 | Component API | HIGH | `api-` |48| 4 | File & Export Layout | MEDIUM | `structure-` |49| 5 | Build Workflow | MEDIUM | `workflow-` |50| 6 | Blocks specifics | MEDIUM | `blocks-` |51| 7 | Debugging | MEDIUM | `debug-` |52| 8 | Testing | HIGH | `testing-` |53| 9 | Shipping | MEDIUM | `ship-` |5455## Quick Reference5657### 1. Component Architecture (CRITICAL)5859- `architecture-compound-by-default` — Multi-part components split into flat-exported60 sub-components, each a `data-slot`; the consumer assembles them (`Dialog`, `Card`).61- `architecture-leaf-uses-variants` — A structureless leaf (button, badge, spinner) stays62 a single component with CVA `variant`/`size` props; do **not** decompose it.63- `architecture-named-slot-props` — A fixed-position addon (trailing icon, action) is an64 explicit named prop, not a child. Avoids `React.Children` inspection.6566### 2. Styling System (HIGH)6768- `styling-cva-variants` — Express variants with `cva()` + `VariantProps`, with69 `defaultVariants`. Export the variants object alongside the component.70- `styling-cn-and-data-slot` — Merge classes with `cn()` from `@aircall/ds`; set a71 `data-slot` on every element; drive state styling with `data-*` attributes.7273### 3. Component API (HIGH)7475- `api-children-and-icons` — Content (incl. icons) flows through `children`, not props.76 Icons imported from `@aircall/react-icons`.77- `api-polymorphism-render` — Polymorphism via the Base UI `render` prop (ds) or78 `useRender` + `mergeProps` (blocks). Never `asChild`.79- `api-typescript-props` — `forwardRef` + `displayName` for interactive components;80 props extend the Base UI primitive's props; variants typed via `VariantProps`.81- `api-data-driven-collections` — A component rendering options from data takes opaque82 `items` + accessor fns (`getItemValue`/`getItemLabel`/`renderItem`, mirror `DataCombobox`),83 string-key values, behavioral flags as `*Keys: Set<string>` props — never a fixed84 `{ value, label, icon, disabled }` item shape. Always pair with a compound escape hatch.85- `api-no-typographic-props` — Typography (font-size/weight/line-height) is a consumer86 `text-*` className, not a prop; no DS-wide type-scale system exists yet. A CVA `size` is for87 control dimensions/density only, never a type scale.88- `api-design-checklist` — Settle the API before writing code: the recurring forks to89 resolve up front (input domain/units, null & non-finite handling, locale/formatting,90 escape-hatch typing, extensibility hook, token mapping) so they don't surface mid-build.9192### 4. File & Export Layout (MEDIUM)9394- `structure-files-and-exports` — Flat kebab-case files in `src/components/`, stories in95 `src/stories/` (PascalCase). Public API is the top-level `src/index.ts` barrel only.96 **No barrel files inside component subdirectories.**9798### 5. Build Workflow (MEDIUM)99100- `workflow-spec-first` — Non-trivial new component (ds **or** blocks): investigate first101 (Figma + real call-sites + sibling components + the primitive you compose), **propose 2-3102 candidate APIs with call-site sketches + a trade-off table + a recommendation, and converge103 before writing code**, then implement + self-verify. Full `specs/<name>.spec.md` via104 `/build-ds-component` for ds primitives; a short in-conversation API proposal for blocks.105106### 6. Blocks specifics (MEDIUM)107108- `blocks-compose-ds` — Import `@aircall/ds` components and `cn` directly; never109 re-implement a primitive. Interactive/renderable blocks must use `useRender` +110 `mergeProps`, matching ds's Base UI patterns.111112### 7. Debugging (MEDIUM)113114- `debug-blocks` — See a block/component render live: start its Storybook from the Hydra115 root (`pnpm sb:dev:blocks` → :6009, `pnpm sb:dev:ds` → :6008), then drive it with the116 `agent-browser-storybook-dev` skill — open the **preview iframe** to render only the117 component (no sidebar/toolbar), read `console`/`errors`, screenshot, iterate via HMR.118119### 8. Testing (HIGH)120121- `testing-stories-are-tests` — There are **no `*.test.tsx` files** — stories ARE the tests.122 `@storybook/addon-vitest` runs every story in a real browser; `play()` = interaction tests.123 a11y runs on every story but is **report-only by default** (`test: 'todo'`) — opt a clean124 component in with `a11y: { test: 'error' }` on its meta to actually gate. Run125 `pnpm --filter @aircall/ds run sb:test -- --run` (CI runs it). Visual regression is Chromatic.126127### 9. Shipping (MEDIUM)128129- `ship-changeset` — ds/blocks are **published packages**; any behavior/API/style change130 needs a changeset (`pnpm changeset` from the Hydra root, or the `add-changeset` skill).131 Pre-1.0 `0.x`: minor for new component/prop, patch for fixes.132133## How to Use134135Read individual rule files for explanations and incorrect/correct examples:136137```138rules/architecture-compound-by-default.md139rules/styling-cva-variants.md140```141142## Related skills143144These Aircall conventions are a concrete dialect of two general skills — consult them145for the deeper "why", and read the divergence notes below before applying them verbatim.146147- **`vercel-composition-patterns`** — the underlying composition philosophy. Aircall's148 `architecture-compound-by-default`, `architecture-leaf-uses-variants` (≈149 `patterns-explicit-variants`), and `api-children-and-icons` (≈150 `patterns-children-over-render-props`) are direct applications.151 ⚠️ **Two divergences:**152 1. That skill's `react19-no-forwardref` says drop `forwardRef` on React 19.153 `@aircall/ds` is on React 19 but **still uses `forwardRef` + `displayName`** as its154 established convention (see `button.tsx`). Follow the codebase, not the rule.155 2. Its `patterns-children-over-render-props` warns against _render-callback_ props156 (`renderItem={() => …}`). The Base UI **`render` prop** Aircall uses is157 element-based polymorphism (`render={<a />}`), a different thing — not in conflict.158159- **`vercel-react-best-practices`** — apply when the component has a performance surface160 (lists, expensive renders, heavy deps). Note its `bundle-barrel-imports` rule is the161 same reasoning behind Aircall's "no barrel files in subdirectories" rule162 (`structure-files-and-exports`).