9.9 KiB
AGENTS.md
Instructions for AI agents working with the HeroUI v3 repository.
Repository Overview
HeroUI v3 is a modern React UI library built with Tailwind CSS v4, organized as a pnpm monorepo managed by Turborepo. Components are built on top of React Aria Components and follow a compound component pattern similar to Radix UI.
Tech Stack
| Technology | Version | Purpose |
|---|---|---|
| Node.js | 22+ | Runtime |
| pnpm | 10.26.2 | Package manager (via corepack) |
| React | 19+ | UI framework |
| Tailwind CSS | 4.x | Styling |
| TypeScript | 5.x | Type safety |
| Turborepo | 2.x | Build orchestration |
| Storybook | Latest | Component development |
| Vitest | 4.x | Testing |
| React Aria Components | Latest | Accessibility primitives |
| tailwind-variants | Latest | Variant-based styling (includes twMerge) |
Monorepo Structure
/
├── apps/
│ └── docs/ # Documentation site (Next.js + Fumadocs)
├── packages/
│ ├── react/ # Main UI library (@heroui/react)
│ │ ├── src/components/ # All components
│ │ ├── src/utils/ # Shared utilities
│ │ └── scripts/ # Build & codegen scripts
│ ├── styles/ # CSS styles & variants (@heroui/styles)
│ │ └── src/components/ # Per-component .css files
│ ├── standard/ # Shared ESLint, Prettier, TS configs
│ ├── storybook/ # Storybook configuration
│ └── vitest/ # Shared Vitest configurations
├── turbo.json
└── pnpm-workspace.yaml
Commands
| Action | Command |
|---|---|
| Install dependencies | pnpm i --hoist |
| Build all packages | pnpm build |
| Build specific package | pnpm build --filter=@heroui/react |
| Dev (Storybook, port 6006) | pnpm dev |
| Dev (Docs site, port 3000) | pnpm dev:docs |
| Lint | pnpm lint |
| Typecheck | pnpm typecheck |
| Test all | pnpm test |
| Test one component | pnpm test button |
| Format | pnpm run format |
| Bump version | pnpm version:bump |
| Scaffold a new component | cd packages/react && pnpm add:component ComponentName |
Git Commit Convention
All commits must follow Conventional Commits and are validated by Husky + commitlint. Pre-commit also runs lint-staged.
<type>(<scope>): <message>
Allowed types: feat, feature, fix, refactor, docs, build, test, ci, chore
Examples:
feat(components): add select component
fix(button): resolve disabled state not applying
docs: update installation guide
Component Architecture
File Structure
Each component lives in packages/react/src/components/<component-name>/:
component-name/
├── component-name.tsx # Component implementation (uses React Aria)
├── component-name.styles.ts # Tailwind Variants styling
├── component-name.stories.tsx # Storybook stories
└── index.ts # Barrel exports
CSS styles live in packages/styles/src/components/<component-name>/.
Creating a New Component
Always use the scaffold script:
cd packages/react
pnpm add:component ComponentName
Then build to update package.json exports:
pnpm build
Compound Component Pattern
HeroUI uses a compound component pattern. Each component exports its sub-parts so users can compose and style them independently.
// Context shares state/styles across parts
const ComponentContext = createContext<{slots?: ReturnType<typeof componentVariants>}>({});
// Root wraps children with context
const ComponentRoot = forwardRef(({children, className, ...props}, ref) => {
const slots = useMemo(() => componentVariants({...}), [...]);
return (
<ComponentContext value={{slots}}>
<ReactAriaPrimitive ref={ref} className={composeTwRenderProps(className, slots.base())}>
{children}
</ReactAriaPrimitive>
</ComponentContext>
);
});
// Child parts consume context
const ComponentItem = forwardRef(({className, ...props}, ref) => {
const {slots} = useContext(ComponentContext);
return (
<ReactAriaPrimitive ref={ref} className={composeTwRenderProps(className, slots?.item())}>
{props.children}
</ReactAriaPrimitive>
);
});
Compound components are exported via Object.assign as the default export:
const CompoundComponent = Object.assign(ComponentRoot, {
Item: ComponentItem,
Trigger: ComponentTrigger,
});
export default CompoundComponent;
Export Strategy
// Named exports for compound components
export * as ComponentName from "./component-name";
// Direct exports for simple components
export {Component, type ComponentProps} from "./component";
// Always export variants
export {componentVariants, type ComponentVariants} from "./component.styles";
Styling Rules
- Styles go in
.styles.tsfiles, never in.tsxfiles. Usetv()fromtailwind-variants. - Import from
tailwind-variants, never from@heroui/standard. - Never use
twMergemanually —tailwind-variantsalready includes it. - Add
"use client"directive at the top of every component.tsxfile. - Display names follow:
HeroUI.ComponentNameorHeroUI.Component.SubPart.
CSS / BEM Naming
Components use BEM-style CSS class names:
- Block:
button,card,alert - Element:
card__header,alert__icon - Modifier:
button--primary,button--lg,button--icon-only
Default Size Pattern (Critical)
All components must include default sizes in base classes so they work without explicit size props:
.avatar {
@apply relative flex size-10 shrink-0 overflow-hidden rounded-full;
/* size-10 is the default (equivalent to --md) */
}
.avatar--sm { @apply size-8; }
.avatar--md { /* empty — this IS the default */ }
.avatar--lg { @apply size-12; }
Interactive State Pattern
All interactive components must support both pseudo-classes and data attributes:
.component {
&:hover,
&[data-hovered="true"] { @apply ...; }
&:active,
&[data-pressed="true"] { @apply ...; }
&:focus-visible,
&[data-focus-visible="true"] {
outline: 2px solid var(--focus);
outline-offset: 2px;
}
}
React Aria className Patterns
React Aria components differ in how they accept className:
- Render-prop components (Button, Checkbox, Switch, Popover, Tooltip, Tabs, Link, Menu, etc.) — use
composeTwRenderProps(className, slots.foo()). - String-only components (Label, Text, Input, TextArea, Heading, Dialog) — pass
classNamedirectly:slots?.label({className}).
Composition Over Duplication
Do not create component-specific Label/Description/FieldError sub-components. Instead, compose with the existing shared primitives:
import {Label} from "@/components/label";
import {Description} from "@/components/description";
<div className="flex items-center gap-3">
<Checkbox id="terms"><Checkbox.Indicator /></Checkbox>
<Label htmlFor="terms">Accept terms</Label>
</div>
Tailwind Class Detection
Tailwind CSS scans files as plain text. Never construct class names dynamically:
// BAD — Tailwind won't detect this
<div className={`text-${color}-600`} />
<span className={`button--${size}`} />
// GOOD — use complete class name mappings
const colorClasses = {
blue: "text-blue-600",
red: "text-red-600",
};
Storybook
All stories must use the "Components" group in their title:
export default { title: "Components/Button" };
Storybook is the primary dev workflow — run with pnpm dev (port 6006).
Icon Library
HeroUI uses Iconify with gravity-ui as the default icon set.
Current Components
Completed
accordion, alert, alert-dialog, autocomplete, avatar, badge, breadcrumbs, button, button-group, card, checkbox, checkbox-group, chip, close-button, color-area, color-field, color-picker, color-slider, color-swatch, color-swatch-picker, combo-box, date-field, date-picker, date-range-picker, description, disclosure, disclosure-group, drawer, dropdown, empty-state, error-message, field-error, fieldset, form, header, input, input-group, input-otp, kbd, label, link, list-box, list-box-item, list-box-section, menu, menu-item, menu-section, meter, modal, number-field, pagination, popover, progress-bar, progress-circle, radio, radio-group, scroll-shadow, search-field, select, separator, skeleton, slider, spinner, surface, switch, switch-group, table, tabs, tag, tag-group, textarea, textfield, time-field, toast, toggle-button, toggle-button-group, toolbar, tooltip, typography
In Progress
calendar, calendar-year-picker, range-calendar
Non-obvious Gotchas
-
pnpm itriggers builds — Thepostinstallhook builds@heroui/stylesand runstypegen:docs. If it fails, runpnpm --filter @heroui/styles buildmanually. -
Build order matters —
@heroui/stylesmust build before@heroui/react. Runningpnpm buildfrom root handles this via Turbo's^builddependency. -
Native addons allowlist — The
pnpm.onlyBuiltDependenciesfield in rootpackage.jsonallows native compilation foresbuild,@swc/core,@parcel/watcher, etc. If this field is missing, you'll see "Ignored build scripts" warnings. -
No tests yet —
pnpm testruns but finds no test files. The Vitest config exists atpackages/vitest. -
Commit hooks — Husky runs
lint-stagedon pre-commit andcommitlinton commit-msg. Non-conforming commits are rejected. -
Run checks before committing —
pnpm lint && pnpm typecheck
Cursor Cloud Specific
- Node.js v22+ is installed via binary tarball to
/usr/local/. - pnpm is activated via
corepack— thepackageManagerfield in rootpackage.jsondeclarespnpm@10.26.2. - Full command reference and component architecture details are also in
CLAUDE.md.