Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# AGENTS.md

## Project

This repo is `headcn/ui`: a shadcn-inspired React UI component system built on top of `@headlessui/react`.

The goal is to provide:

- copyable, editable components
- shadcn-like CLI + registry architecture
- `components.json` support
- Tailwind-first styling
- accessible behavior powered by Headless UI

Headless UI is unstyled and exposes state through render props/data attributes, so components in this repo must provide the visual/styling layer while preserving Headless UI accessibility behavior.

## Current component coverage

Implemented:

- Button
- Checkbox
- Combobox
- Fieldset
- Input
- Listbox
- Radio Group
- Textarea

Missing / planned:

- Dropdown Menu
- Disclosure
- Dialog
- Popover
- Tabs
- Transition
- Select
- Switch

Use Headless UI React docs as the source of truth for APIs and accessibility behavior.

## Design principles

1. Follow shadcn-style architecture.
- Components are copied into user apps, not hidden inside an opaque package.
- Code should be readable, editable, and idiomatic.
- Prefer composition over configuration-heavy APIs.

2. Use Headless UI primitives.
- Import primitives from `@headlessui/react`.
- Do not replace Headless UI behavior with custom keyboard/focus logic unless absolutely necessary.
- Preserve accessibility semantics.

3. Tailwind-first styling.
- Use Tailwind classes.
- Use `cn(...)` for className merging.
- Support user-provided `className`.
- Prefer Headless UI `data-*` attributes for state styling.

4. Match existing repo conventions.
- Before adding a component, inspect nearby implemented components.
- Follow existing file names, exports, class patterns, registry format, docs format, and tests.

## Component rules

When creating or editing a component:

- Use TypeScript.
- Use React `forwardRef` where appropriate.
- Export all public subcomponents.
- Preserve native props where possible.
- Keep APIs close to Headless UI naming unless the repo already has a different convention.
- Add `"use client"` when the component requires client interactivity.
- Do not over-abstract.
- Do not introduce new dependencies unless clearly justified.
- Do not hardcode app-specific copy, icons, routes, or business logic.
- Use accessible defaults.
- Ensure keyboard interaction is delegated to Headless UI.

## Styling rules

- Use `cn()` for conditional class names.
- Keep default styling consistent with existing components.
- Use `data-[state]`, `data-focus`, `data-hover`, `data-open`, `data-checked`, `data-disabled`, etc. where Headless UI exposes them.
- Do not use Radix-specific attributes like `data-state="open"` unless this repo already maps them intentionally.
- Avoid global CSS unless the component pattern requires it.
- Do not add animation libraries for basic transitions.

## Registry / CLI rules

When adding a component, update every required registry/CLI artifact used by the repo.

Likely files to inspect:

- registry files
- component metadata
- CLI add/init logic
- docs navigation
- examples
- package exports
- tests

The component should be installable through the same path as existing components.

## Quality checklist

Before finishing a task, verify:

- Typecheck passes.
- Lint passes.
- Build passes.
- Component exports are correct.
- Registry entry exists.
- Docs/example exists.
- Component works with keyboard navigation.
- `className` overrides work.
- Disabled/error/focus/open/checked states are styled where relevant.
- No Radix imports were added.

## Commands

Use the repo’s package manager. Detect it from lockfiles.

Common commands to try:

- `pnpm install`
- `pnpm lint`
- `pnpm typecheck`
- `pnpm build`
- `pnpm test`

Do not invent scripts. Check `package.json` first.

## Pull request expectations

Every component PR should include:

- component implementation
- registry metadata
- docs/example
- export updates
- validation notes

Use concise PR summaries:

- What component was added/changed
- Which Headless UI primitives it wraps
- Which commands passed
47 changes: 47 additions & 0 deletions apps/www/content/docs/components/switch.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
title: Switch
description: Switches are used to toggle between two states, usually representing on and off.
links:
doc: https://headlessui.com/react/switch
api: https://headlessui.com/react/switch#component-api
catetory: form
---

<ComponentPreview name="switch-demo" />

## Installation

<CodeBlockCommand command="headcn@latest add switch" />

## Usage

Switches are built using the `Switch` component. You can toggle your switch by clicking directly on the component, or by pressing the spacebar while it's focused.

Toggling the switch calls the `onChange` function with a negated version of the `checked` value.

```tsx
import { useState } from "react"
import { Switch } from "@/components/ui/switch"

export default function Example() {
const [enabled, setEnabled] = useState(false)

return <Switch checked={enabled} onChange={setEnabled} />
}
```

## Examples

### Description

<ComponentPreview name="switch-with-description" />

### Choice Card

Card-style selection where `Field` wraps the entire section for a clickable card pattern.

<ComponentPreview name="switch-choice-card" />

### Disabled

<ComponentPreview name="switch-disabled" />
2 changes: 1 addition & 1 deletion apps/www/public/r/components/button-demo.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"files": [
{
"path": "registry/examples/button-demo.tsx",
"content": "import { Button } from \"@/registry/ui/button\"\n\nexport default function ButtonDemo() {\n return <Button>Button</Button>\n}\n",
"content": "import { Button } from \"@/registry/ui/button\"\r\n\r\nexport default function ButtonDemo() {\r\n return <Button>Button</Button>\r\n}\r\n",
"type": "registry:example"
}
]
Expand Down
2 changes: 1 addition & 1 deletion apps/www/public/r/components/button-destructive.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"files": [
{
"path": "registry/examples/button-destructive.tsx",
"content": "import { Button } from \"@/registry/ui/button\"\n\nexport default function ButtonDestructive() {\n return <Button variant={\"destructive\"}>Destructive</Button>\n}\n",
"content": "import { Button } from \"@/registry/ui/button\"\r\n\r\nexport default function ButtonDestructive() {\r\n return <Button variant={\"destructive\"}>Destructive</Button>\r\n}\r\n",
"type": "registry:example"
}
]
Expand Down
2 changes: 1 addition & 1 deletion apps/www/public/r/components/button-ghost.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"files": [
{
"path": "registry/examples/button-ghost.tsx",
"content": "import { Button } from \"@/registry/ui/button\"\n\nexport default function ButtonGhost() {\n return <Button variant={\"ghost\"}>Ghost</Button>\n}\n",
"content": "import { Button } from \"@/registry/ui/button\"\r\n\r\nexport default function ButtonGhost() {\r\n return <Button variant={\"ghost\"}>Ghost</Button>\r\n}\r\n",
"type": "registry:example"
}
]
Expand Down
2 changes: 1 addition & 1 deletion apps/www/public/r/components/button-icon.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"files": [
{
"path": "registry/examples/button-icon.tsx",
"content": "import { Button } from \"@/registry/ui/button\"\nimport { StarIcon } from \"@heroicons/react/16/solid\"\n\nexport default function ButtonIcon() {\n return (\n <Button variant={\"outline\"} size={\"icon-md\"}>\n <StarIcon className=\"size-5\" />\n </Button>\n )\n}\n",
"content": "import { Button } from \"@/registry/ui/button\"\r\nimport { StarIcon } from \"@heroicons/react/16/solid\"\r\n\r\nexport default function ButtonIcon() {\r\n return (\r\n <Button variant={\"outline\"} size={\"icon-md\"}>\r\n <StarIcon className=\"size-5\" />\r\n </Button>\r\n )\r\n}\r\n",
"type": "registry:example"
}
]
Expand Down
2 changes: 1 addition & 1 deletion apps/www/public/r/components/button-link.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"files": [
{
"path": "registry/examples/button-link.tsx",
"content": "import { Button } from \"@/registry/ui/button\"\n\nexport default function ButtonLink() {\n return <Button variant={\"link\"}>Link</Button>\n}\n",
"content": "import { Button } from \"@/registry/ui/button\"\r\n\r\nexport default function ButtonLink() {\r\n return <Button variant={\"link\"}>Link</Button>\r\n}\r\n",
"type": "registry:example"
}
]
Expand Down
2 changes: 1 addition & 1 deletion apps/www/public/r/components/button-secondary.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"files": [
{
"path": "registry/examples/button-secondary.tsx",
"content": "import { Button } from \"@/registry/ui/button\"\n\nexport default function ButtonSecondary() {\n return <Button variant={\"secondary\"}>Secondary</Button>\n}\n",
"content": "import { Button } from \"@/registry/ui/button\"\r\n\r\nexport default function ButtonSecondary() {\r\n return <Button variant={\"secondary\"}>Secondary</Button>\r\n}\r\n",
"type": "registry:example"
}
]
Expand Down
2 changes: 1 addition & 1 deletion apps/www/public/r/components/button-with-icon.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"files": [
{
"path": "registry/examples/button-with-icon.tsx",
"content": "import { Button } from \"@/registry/ui/button\"\nimport { CloudArrowUpIcon } from \"@heroicons/react/16/solid\"\n\nexport default function ButtonWithIcon() {\n return (\n <Button>\n <CloudArrowUpIcon className=\"size-5\" /> Upload\n </Button>\n )\n}\n",
"content": "import { Button } from \"@/registry/ui/button\"\r\nimport { CloudArrowUpIcon } from \"@heroicons/react/16/solid\"\r\n\r\nexport default function ButtonWithIcon() {\r\n return (\r\n <Button>\r\n <CloudArrowUpIcon className=\"size-5\" /> Upload\r\n </Button>\r\n )\r\n}\r\n",
"type": "registry:example"
}
]
Expand Down
2 changes: 1 addition & 1 deletion apps/www/public/r/components/button.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"files": [
{
"path": "registry/ui/button.tsx",
"content": "import {\n Button as HeadlessButton,\n type ButtonProps as HeadlessButtonProps,\n} from \"@headlessui/react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst buttonVariants = cva(\n \"inline-flex shrink-0 items-center justify-center gap-1.5 rounded-md text-sm font-semibold whitespace-nowrap transition data-active:translate-y-0.5 data-disabled:pointer-events-none data-disabled:opacity-50\",\n {\n variants: {\n variant: {\n primary: \"bg-primary text-primary-foreground data-hover:bg-primary/90\",\n secondary:\n \"bg-secondary text-secondary-foreground data-hover:bg-secondary/90\",\n destructive:\n \"bg-destructive text-foreground data-hover:bg-destructive/90\",\n outline:\n \"bg-accent/50 text-accent-foreground data-hover:bg-accent border\",\n ghost: \"data-hover:bg-accent text-accent-foreground\",\n link: \"text-primary underline-offset-4 data-hover:underline\",\n },\n size: {\n xs: \"h-7 px-2.5 text-xs has-[>svg]:px-2\",\n sm: \"h-8 px-3 has-[>svg]:px-2.5\",\n md: \"h-9 px-4 has-[>svg]:px-3\",\n lg: \"h-10 px-5 has-[>svg]:px-4\",\n \"icon-xs\": \"size-7\",\n \"icon-sm\": \"size-8\",\n \"icon-md\": \"size-9\",\n \"icon-lg\": \"size-10\",\n },\n },\n defaultVariants: {\n variant: \"primary\",\n size: \"md\",\n },\n }\n)\n\nfunction Button({\n children,\n className,\n variant,\n size,\n ...props\n}: HeadlessButtonProps & VariantProps<typeof buttonVariants>) {\n return (\n <HeadlessButton\n className={cn(buttonVariants({ variant, size, className }))}\n {...props}\n >\n {children}\n </HeadlessButton>\n )\n}\n\nexport { Button, buttonVariants }\n",
"content": "import {\r\n Button as HeadlessButton,\r\n type ButtonProps as HeadlessButtonProps,\r\n} from \"@headlessui/react\"\r\nimport { cva, type VariantProps } from \"class-variance-authority\"\r\n\r\nimport { cn } from \"@/lib/utils\"\r\n\r\nconst buttonVariants = cva(\r\n \"inline-flex shrink-0 items-center justify-center gap-1.5 rounded-md text-sm font-semibold whitespace-nowrap transition data-active:translate-y-0.5 data-disabled:pointer-events-none data-disabled:opacity-50\",\r\n {\r\n variants: {\r\n variant: {\r\n primary: \"bg-primary text-primary-foreground data-hover:bg-primary/90\",\r\n secondary:\r\n \"bg-secondary text-secondary-foreground data-hover:bg-secondary/90\",\r\n destructive:\r\n \"bg-destructive text-foreground data-hover:bg-destructive/90\",\r\n outline:\r\n \"bg-accent/50 text-accent-foreground data-hover:bg-accent border\",\r\n ghost: \"data-hover:bg-accent text-accent-foreground\",\r\n link: \"text-primary underline-offset-4 data-hover:underline\",\r\n },\r\n size: {\r\n xs: \"h-7 px-2.5 text-xs has-[>svg]:px-2\",\r\n sm: \"h-8 px-3 has-[>svg]:px-2.5\",\r\n md: \"h-9 px-4 has-[>svg]:px-3\",\r\n lg: \"h-10 px-5 has-[>svg]:px-4\",\r\n \"icon-xs\": \"size-7\",\r\n \"icon-sm\": \"size-8\",\r\n \"icon-md\": \"size-9\",\r\n \"icon-lg\": \"size-10\",\r\n },\r\n },\r\n defaultVariants: {\r\n variant: \"primary\",\r\n size: \"md\",\r\n },\r\n }\r\n)\r\n\r\nfunction Button({\r\n children,\r\n className,\r\n variant,\r\n size,\r\n ...props\r\n}: HeadlessButtonProps & VariantProps<typeof buttonVariants>) {\r\n return (\r\n <HeadlessButton\r\n className={cn(buttonVariants({ variant, size, className }))}\r\n {...props}\r\n >\r\n {children}\r\n </HeadlessButton>\r\n )\r\n}\r\n\r\nexport { Button, buttonVariants }\r\n",
"type": "registry:ui"
}
]
Expand Down
2 changes: 1 addition & 1 deletion apps/www/public/r/components/checkbox-demo.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"files": [
{
"path": "registry/examples/checkbox-demo.tsx",
"content": "import { Checkbox } from \"@/registry/ui/checkbox\"\nimport { Description } from \"@/registry/ui/description\"\nimport { Field } from \"@/registry/ui/field\"\nimport { Label } from \"@/registry/ui/label\"\n\nexport default function CheckboxDemo() {\n return (\n <div className=\"grid gap-4\">\n <Field className=\"flex items-center gap-3\">\n <Checkbox />\n <Label>Accept terms and conditions</Label>\n </Field>\n <Field className=\"flex items-center gap-3\">\n <Checkbox defaultChecked />\n <div>\n <Label>Accept terms and conditions</Label>\n <Description>\n This will give you early access to new features we&apos;re\n developing.\n </Description>\n </div>\n </Field>\n <Field disabled className=\"flex items-center gap-3\">\n <Checkbox />\n <Label>Enable notifications</Label>\n </Field>\n <Field>\n <Label className=\"bg-accent/25 has-[[data-checked]]:border-primary/25 has-[[data-checked]]:bg-primary/25 flex items-start gap-3 rounded-md border p-3 transition-colors\">\n <Checkbox defaultChecked />\n <div>\n <Label>Enable notifications</Label>\n <Description className=\"font-normal\">\n You can enable or disable notifications at any time.\n </Description>\n </div>\n </Label>\n </Field>\n </div>\n )\n}\n",
"content": "import { Checkbox } from \"@/registry/ui/checkbox\"\r\nimport { Description } from \"@/registry/ui/description\"\r\nimport { Field } from \"@/registry/ui/field\"\r\nimport { Label } from \"@/registry/ui/label\"\r\n\r\nexport default function CheckboxDemo() {\r\n return (\r\n <div className=\"grid gap-4\">\r\n <Field className=\"flex items-center gap-3\">\r\n <Checkbox />\r\n <Label>Accept terms and conditions</Label>\r\n </Field>\r\n <Field className=\"flex items-center gap-3\">\r\n <Checkbox defaultChecked />\r\n <div>\r\n <Label>Accept terms and conditions</Label>\r\n <Description>\r\n This will give you early access to new features we&apos;re\r\n developing.\r\n </Description>\r\n </div>\r\n </Field>\r\n <Field disabled className=\"flex items-center gap-3\">\r\n <Checkbox />\r\n <Label>Enable notifications</Label>\r\n </Field>\r\n <Field>\r\n <Label className=\"bg-accent/25 has-[[data-checked]]:border-primary/25 has-[[data-checked]]:bg-primary/25 flex items-start gap-3 rounded-md border p-3 transition-colors\">\r\n <Checkbox defaultChecked />\r\n <div>\r\n <Label>Enable notifications</Label>\r\n <Description className=\"font-normal\">\r\n You can enable or disable notifications at any time.\r\n </Description>\r\n </div>\r\n </Label>\r\n </Field>\r\n </div>\r\n )\r\n}\r\n",
"type": "registry:example"
}
]
Expand Down
2 changes: 1 addition & 1 deletion apps/www/public/r/components/checkbox.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"files": [
{
"path": "registry/ui/checkbox.tsx",
"content": "import {\n Checkbox as HeadlessCheckbox,\n type CheckboxProps as HeadlessCheckboxProps,\n} from \"@headlessui/react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Checkbox({ className, ...props }: HeadlessCheckboxProps) {\n return (\n <HeadlessCheckbox\n className={cn(\n \"group bg-accent/50 grid size-4 shrink-0 place-items-center rounded border\",\n \"data-checked:bg-primary data-disabled:cursor-not-allowed data-disabled:opacity-50\",\n className\n )}\n {...props}\n >\n <svg\n className={cn(\n \"stroke-primary-foreground size-[90%] translate-y-0.5 opacity-0 transition\",\n \"group-data-checked:translate-y-0 group-data-checked:opacity-100\"\n )}\n viewBox=\"0 0 14 14\"\n fill=\"none\"\n >\n <path\n d=\"M3 8L6 11L11 3.5\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </HeadlessCheckbox>\n )\n}\n\nexport { Checkbox }\n",
"content": "import {\r\n Checkbox as HeadlessCheckbox,\r\n type CheckboxProps as HeadlessCheckboxProps,\r\n} from \"@headlessui/react\"\r\n\r\nimport { cn } from \"@/lib/utils\"\r\n\r\nfunction Checkbox({ className, ...props }: HeadlessCheckboxProps) {\r\n return (\r\n <HeadlessCheckbox\r\n className={cn(\r\n \"group bg-accent/50 grid size-4 shrink-0 place-items-center rounded border\",\r\n \"data-checked:bg-primary data-disabled:cursor-not-allowed data-disabled:opacity-50\",\r\n className\r\n )}\r\n {...props}\r\n >\r\n <svg\r\n className={cn(\r\n \"stroke-primary-foreground size-[90%] translate-y-0.5 opacity-0 transition\",\r\n \"group-data-checked:translate-y-0 group-data-checked:opacity-100\"\r\n )}\r\n viewBox=\"0 0 14 14\"\r\n fill=\"none\"\r\n >\r\n <path\r\n d=\"M3 8L6 11L11 3.5\"\r\n strokeWidth={2}\r\n strokeLinecap=\"round\"\r\n strokeLinejoin=\"round\"\r\n />\r\n </svg>\r\n </HeadlessCheckbox>\r\n )\r\n}\r\n\r\nexport { Checkbox }\r\n",
"type": "registry:ui"
}
]
Expand Down
Loading
Loading