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,useRendermandatory). ⚠️ Stale on layout: it says per-component dirs +__tests__/, but the real convention is flat kebab-case files + stories-as-tests — truststructure-files-and-exports/testing-stories-are-testsbelow 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/dsor 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 adata-slot; the consumer assembles them (Dialog,Card).architecture-leaf-uses-variants— A structureless leaf (button, badge, spinner) stays a single component with CVAvariant/sizeprops; do not decompose it.architecture-named-slot-props— A fixed-position addon (trailing icon, action) is an explicit named prop, not a child. AvoidsReact.Childreninspection.
2. Styling System (HIGH)
styling-cva-variants— Express variants withcva()+VariantProps, withdefaultVariants. Export the variants object alongside the component.styling-cn-and-data-slot— Merge classes withcn()from@aircall/ds; set adata-sloton every element; drive state styling withdata-*attributes.
3. Component API (HIGH)
api-children-and-icons— Content (incl. icons) flows throughchildren, not props. Icons imported from@aircall/react-icons.api-polymorphism-render— Polymorphism via the Base UIrenderprop (ds) oruseRender+mergeProps(blocks). NeverasChild.api-typescript-props—forwardRef+displayNamefor interactive components; props extend the Base UI primitive's props; variants typed viaVariantProps.api-data-driven-collections— A component rendering options from data takes opaqueitems+ accessor fns (getItemValue/getItemLabel/renderItem, mirrorDataCombobox), 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 consumertext-*className, not a prop; no DS-wide type-scale system exists yet. A CVAsizeis 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 insrc/components/, stories insrc/stories/(PascalCase). Public API is the top-levelsrc/index.tsbarrel 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. Fullspecs/<name>.spec.mdvia/build-ds-componentfor ds primitives; a short in-conversation API proposal for blocks.
6. Blocks specifics (MEDIUM)
blocks-compose-ds— Import@aircall/dscomponents andcndirectly; never re-implement a primitive. Interactive/renderable blocks must useuseRender+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 theagent-browser-storybook-devskill — open the preview iframe to render only the component (no sidebar/toolbar), readconsole/errors, screenshot, iterate via HMR.
8. Testing (HIGH)
testing-stories-are-tests— There are no*.test.tsxfiles — stories ARE the tests.@storybook/addon-vitestruns 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 witha11y: { test: 'error' }on its meta to actually gate. Runpnpm --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 changesetfrom the Hydra root, or theadd-changesetskill). Pre-1.00.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'sarchitecture-compound-by-default,architecture-leaf-uses-variants(≈patterns-explicit-variants), andapi-children-and-icons(≈patterns-children-over-render-props) are direct applications. ⚠️ Two divergences:- That skill's
react19-no-forwardrefsays dropforwardRefon React 19.@aircall/dsis on React 19 but still usesforwardRef+displayNameas its established convention (seebutton.tsx). Follow the codebase, not the rule. - Its
patterns-children-over-render-propswarns against render-callback props (renderItem={() => …}). The Base UIrenderprop Aircall uses is element-based polymorphism (render={<a />}), a different thing — not in conflict.
- That skill's
-
vercel-react-best-practices— apply when the component has a performance surface (lists, expensive renders, heavy deps). Note itsbundle-barrel-importsrule is the same reasoning behind Aircall's "no barrel files in subdirectories" rule (structure-files-and-exports).