This file provides guidance for AI coding agents working on the Nuxt UI repository.
Nuxt UI is a component library built on Reka UI, Tailwind CSS, and Tailwind Variants. It provides accessible, themeable components for both Nuxt and Vue applications.
src/
├── runtime/
│ ├── components/ # Vue components (PascalCase.vue)
│ ├── composables/ # Composables (use*.ts)
│ ├── types/ # TypeScript types
│ └── utils/ # Utility functions
├── theme/ # Tailwind Variants themes (kebab-case.ts)
└── module.ts
test/
├── bundle/ # Bundle size fixtures (`pnpm run size`)
├── components/ # Component tests (*.spec.ts)
│ └── __snapshots__/ # Auto-generated snapshots
└── component-render.ts
docs/
└── content/docs/2.components/ # Documentation (*.md)
playgrounds/
└── nuxt/app/pages/components/ # Playground pages
pnpm run dev:prepare # Generate type stubs (run after install)
pnpm run dev # Nuxt playground
pnpm run dev:vue # Vue playground
pnpm run repl # REPL playground
pnpm run docs # Documentation site
pnpm run lint # Check linting
pnpm run lint:fix # Fix linting
pnpm run typecheck # Type checking
pnpm run test # Run tests
pnpm run size # Build test/bundle apps and print their JS/CSS sizesUse the CLI to create new components:
pnpm cli make component <name> [options]Options:
--primitive- Primitive component (uses Reka UI Primitive)--prose- Prose/typography component--content- Content component--template- Generate specific template only (playground,docs,test,theme,component)
- Conventional commits: All commit messages must follow conventional commits (e.g.
fix(Button): resolve hover state,feat(Modal): add fullscreen prop). - Semantic colors: Use
text-default,bg-elevated, etc. — never raw Tailwind palette colors liketext-gray-500. Soonbadge on docs headings: PRs that introduce a new feature or fix often add:badge{label="Soon" class="align-text-top"}to the relevant docs heading. This is intentional: the docs site redeploys on merge, but the feature only ships on the next npm release — the badge bridges that gap. Do NOT flag this as inconsistent in reviews. See documentation.md for details.- Two build adapters: Build-side changes (templates, auto-imports, icons, component detection, build plugins) must be checked against both adapters:
src/module.tsfor Nuxt,src/unplugin.tsandsrc/plugins/*for Vue. Shared logic belongs insrc/utils/. In code that runs from the published build, only the two entry files may resolve paths fromimport.meta.url, since bundled files can land in any output chunk. Everything else anchors onruntimeDir.
The following conventions and references apply only when working on files in src/ or test/. They do not apply to docs/, playgrounds/, or other directories.
Load these based on your task. Do not load all files at once — only load what's relevant.
| File | Topics |
|---|---|
| .github/contributing/component-structure.md | Vue component file patterns, props/slots/emits interfaces, script setup |
| .github/contributing/theme-structure.md | Tailwind Variants theme files, slots, variants, compoundVariants |
| .github/contributing/testing.md | Vitest patterns, snapshot testing, accessibility testing |
| .github/contributing/documentation.md | Component docs structure, MDC syntax, examples |
| Convention | Description |
|---|---|
| Type imports | Always separate: import type { X } on its own line |
| Props defaults | Use withDefaults() for runtime, JSDoc @defaultValue for docs |
| Template slots | Add data-slot="name" attributes on all elements |
data-slot on root |
A caller-supplied data-slot must win on the component's root (component's own value as fallback); inner elements keep theirs. Single-root components with default inheritAttrs get this free via Vue fallthrough. For inheritAttrs: false, place the default before the root's v-bind (data-slot="root" v-bind="$attrs"), or read ($attrs['data-slot'] as string | undefined) ?? 'root' on the root when $attrs is forwarded to an inner element. See component-structure.md. |
| Computed ui | Always use computed(() => tv(...)) for reactive theming |
| Theme defaults | Wrap raw props with useComponentProps(name, _props) to resolve the priority chain (explicit prop > <UTheme :props> > withDefaults > app.config.ui.<name>.defaultVariants). The proxy deep-merges ui automatically — read props.ui?.<slot> in templates. theme.defaultVariants is not read by the proxy — it only feeds tv() class resolution. Pass the raw _props (not the proxy) to useFormField / useFieldGroup / useAvatarGroup so their injection precedence (closer context wins) stays correct. |
| Form/group fallback | When consuming size / color / highlight from useFormField, useFieldGroup, or useAvatarGroup, always fall back to the proxy in tv() calls: size: size.value ?? props.size, color: color.value ?? props.color, highlight: highlight.value ?? props.highlight. This gives the full precedence explicit > group/formField > <UTheme :props> > undefined. Without the ?? props.X fallback, <UTheme :props> is silently dropped when the closer context (FormField/FieldGroup/AvatarGroup) is absent. |
| Semantic colors | Use text-default, bg-elevated, etc. - never Tailwind palette |
| Logical properties (RTL) | Use logical utilities (ms/me, ps/pe, start/end, text-start/end, border-s/e, rounded-s/e) not physical (ml/mr, left/right, text-left/right) so components work in RTL by default. transform/cursor/gradients/transitions need explicit rtl: counterparts. See theme-structure.md. |
| Reka UI props | Use reactivePick + useForwardProps(source, emits?) from composables/useForwardProps to forward props (proxy-aware; reka-ui's useForwardProps / useForwardPropsEmits filter out <UTheme :props> defaults) |
| Form components | Use useFormField and useFieldGroup composables |
Copy this checklist and track progress when creating a new component:
Component: [name]
Progress:
- [ ] 1. Scaffold with CLI: pnpm cli make component <name>
- [ ] 2. Implement component in src/runtime/components/
- [ ] 3. Create theme in src/theme/
- [ ] 4. Export types from src/runtime/types/index.ts
- [ ] 5. Register in ThemeDefaults interface (src/runtime/types/theme.ts)
- [ ] 6. Write tests in test/components/
- [ ] 7. Create docs in docs/content/docs/2.components/
- [ ] 8. Add playground page
- [ ] 9. Run pnpm run lint
- [ ] 10. Run pnpm run typecheck
- [ ] 11. Run pnpm run test
When reviewing PRs that touch src/ or test/, verify:
PR Review:
- [ ] Component follows existing patterns (see .github/contributing/)
- [ ] Theme uses semantic colors, not Tailwind palette
- [ ] Tests cover props, slots, and accessibility
- [ ] Documentation includes Usage, Examples, and API sections
- [ ] Conventional commit message format
- [ ] All checks pass (lint, typecheck, test)
Do NOT flag as issues:
:badge{label="Soon"}on docs headings in PRs adding new features/fixes (intentional — bridges the gap between docs deploy on merge and feature shipping on next npm release).
-
pnpm run lintpasses -
pnpm run typecheckpasses -
pnpm run testpasses - Documentation is updated if applicable
- Commit message follows conventional commits
- PR description follows .github/PULL_REQUEST_TEMPLATE.md (linked issue, type of change, description, checklist)
Multiple commits are fine — PRs are squash merged, so no need to rebase or force push.