Base UI component patterns and design system guidelines. Use when creating or styling UI components, managing spacing, typography, border radius, icons, or following project design standards.
Resources
1Install
npx skillscat add retrip-ai/agent-skills/base-ui-design Install via the SkillsCat registry.
Base UI Design System
Complete guide to the project's design system using Base UI components and strict design guidelines.
Overview
This skill covers:
- Base UI Components - Unstyled, accessible React primitives
- Design Principles - Typography, spacing, border radius standards
- Form Patterns - Edit forms, UnsavedChangesBar integration
- Component Usage - Custom UI components in
@/components/ui
When to Apply
Reference these guidelines when:
- Creating new UI components
- Styling existing components
- Working with forms (especially edit forms)
- Managing spacing, typography, or icons
- Ensuring accessibility compliance
- Following design consistency
Quick Reference
Design Principles (CRITICAL)
| Principle | Rule | Valid Values |
|---|---|---|
| Typography | Use default sizes ONLY | No text-sm, text-xs, etc. |
| Spacing | Powers of 2 | gap-2, gap-4, gap-8, gap-16 |
| Border Radius | Always rounded-md |
No rounded-lg, rounded-full |
| Spacing Utils | Use flex gap | flex flex-col gap-* NOT space-y-* |
| Icons | Icon suffix | MonitorIcon NOT Monitor |
Component Hierarchy
@base-ui/react # Unstyled primitives
↓
@/components/ui # Custom styled components
↓
Page components # App-specific usageCommon Components
Dialog:
import * as Dialog from '@base-ui/react/Dialog';
<Dialog.Root>
<Dialog.Trigger className="px-4 py-2 bg-blue-500 rounded-md">
Open
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Backdrop className="fixed inset-0 bg-black/50" />
<Dialog.Popup className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-white rounded-md p-6">
<Dialog.Title className="font-semibold mb-4">Title</Dialog.Title>
<Dialog.Description className="mb-4">Description</Dialog.Description>
<Dialog.Close className="px-4 py-2 bg-gray-200 rounded-md">Close</Dialog.Close>
</Dialog.Popup>
</Dialog.Portal>
</Dialog.Root>Popover:
import * as Popover from '@base-ui/react/Popover';
<Popover.Root>
<Popover.Trigger>Open</Popover.Trigger>
<Popover.Portal>
<Popover.Popup className="bg-white rounded-md p-4">
Content
</Popover.Popup>
</Popover.Portal>
</Popover.Root>References
Complete documentation with examples:
references/components.md- Base UI components, custom UI, form patterns, accessibility
To find specific patterns:
grep -l "Dialog" references/*.md
grep -l "UnsavedChangesBar" references/*.md
grep -l "spacing" references/*.mdCore Principles
1. Typography - Default Sizes Only
❌ WRONG - Custom text sizes:
<p className="text-sm">Small text</p>
<span className="text-xs">Tiny text</span>
<h1 className="text-2xl">Heading</h1>✅ CORRECT - Default sizes:
<p>Regular text</p>
<span>Regular text</span>
<h1>Heading</h1>Let component hierarchy and semantic HTML define visual hierarchy naturally.
2. Spacing - Powers of 2 Only
❌ WRONG - Space utilities or odd gaps:
<div className="space-y-6">...</div>
<div className="flex gap-3">...</div>
<div className="flex gap-6">...</div>✅ CORRECT - Flex gap with powers of 2:
<div className="flex flex-col gap-8">...</div>
<div className="flex gap-4">...</div>
<div className="flex gap-16">...</div>Valid gap values: gap-2, gap-4, gap-8, gap-16 (in px: 8, 16, 32, 64)
3. Border Radius - Always rounded-md
❌ WRONG - Other rounded values:
<div className="rounded-lg">...</div>
<div className="rounded-full">...</div>
<button className="rounded-xl">...</button>✅ CORRECT - Only rounded-md:
<div className="rounded-md">...</div>
<div className="rounded-md">...</div>
<button className="rounded-md">...</button>4. Icons - Always Use Icon Suffix
❌ WRONG - Import without Icon suffix:
import { Monitor, Moon, Sun } from 'lucide-react';
<Monitor className="w-4 h-4" />✅ CORRECT - Icon suffix:
import { MonitorIcon, MoonIcon, SunIcon } from 'lucide-react';
import { BadgeCheckIcon, Building2Icon, UsersIcon } from 'lucide-react';
<MonitorIcon className="w-4 h-4" />This ensures consistent naming and clarity.
Form Patterns
Edit Forms (UnsavedChangesBar)
When to use: Modifying existing data (profiles, settings, configurations)
Key features:
- Bar appears when form becomes dirty
- Bar disappears when changes saved or discarded
- Validation on
onBlurfor fields,onSubmitfor form - Only calls tRPC mutation on submit
import { useForm, useFormState, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { UnsavedChangesBar } from '@/components/unsaved-changes-bar';
import { Field, FieldLabel, FieldError } from '@/components/ui/field';
function EditForm({ initialData }: Props) {
const form = useForm({
defaultValues: initialData,
resolver: zodResolver(schema),
mode: 'onBlur', // Validate on blur
});
const { isDirty, isSubmitting } = useFormState({ control: form.control });
const mutation = useMutation(trpc.organizations.update.mutationOptions());
const isSaving = isSubmitting || mutation.isPending;
const onSubmit = form.handleSubmit(async (value) => {
await mutation.mutateAsync(value);
form.reset(value); // Reset with new values to clear isDirty
});
const handleDiscard = () => form.reset(); // Return to initial state
return (
<form onSubmit={onSubmit}>
<Controller
name="name"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid || undefined}>
<FieldLabel>Name</FieldLabel>
<Input {...field} aria-invalid={fieldState.invalid || undefined} />
{fieldState.error && (
<FieldError errors={[{ message: fieldState.error.message || '' }]} />
)}
</Field>
)}
/>
<UnsavedChangesBar
show={isDirty}
isSaving={isSaving}
onDiscard={handleDiscard}
labels={{
unsavedChanges: 'Unsaved changes',
discard: 'Discard',
save: 'Save',
}}
/>
</form>
);
}Critical requirements:
- Use
useFormState({ control: form.control })for reactiveisDirty - Use
Controllerfor controlled inputs - Use
Field,FieldLabel,FieldErrorcomponents - Set
mode: 'onBlur'for field validation - Call
form.reset(value)after successful save
Create Forms (No UnsavedChangesBar)
When to use: Creating new entities (API keys, invitations, new organizations)
Key features:
- Submit immediately, no save bar
- Validation on
onBlur - Simpler pattern
function CreateForm() {
const form = useForm({
defaultValues: { name: '' },
resolver: zodResolver(schema),
mode: 'onBlur',
});
const mutation = useMutation(trpc.apiKeys.create.mutationOptions());
const onSubmit = form.handleSubmit(async (value) => {
await mutation.mutateAsync(value);
form.reset();
});
return (
<form onSubmit={onSubmit}>
<Controller
name="name"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid || undefined}>
<FieldLabel>Name</FieldLabel>
<Input {...field} />
{fieldState.error && <FieldError errors={[{ message: fieldState.error.message || '' }]} />}
</Field>
)}
/>
<button disabled={mutation.isPending}>
{mutation.isPending ? 'Creating...' : 'Create'}
</button>
</form>
);
}Base UI Components
Available Components
| Component | Usage |
|---|---|
| Dialog | Modals, alerts, confirmations |
| Popover | Tooltips, context menus |
| Select | Dropdowns, option selection |
| Tabs | Tab navigation |
| Checkbox | Boolean inputs |
| Switch | Toggle switches |
| Slider | Range inputs |
All components are:
- Unstyled - You control all styles
- Accessible - ARIA compliant, keyboard navigation
- Composable - Build complex UIs with simple primitives
Custom UI Components
Located in @/components/ui:
src/components/ui/
├── button.tsx # Button variants
├── dialog.tsx # Pre-styled dialogs
├── field.tsx # Form field components
├── input.tsx # Input fields
├── select.tsx # Select dropdowns
└── ...Prefer custom UI components when available - they follow design guidelines automatically.
Accessibility
Base UI provides:
- Keyboard Navigation - Tab, Arrow keys, Escape, Enter
- ARIA Attributes - Proper roles, labels, descriptions
- Screen Readers - State change announcements
- Focus Management - Traps focus in modals/dialogs
Don't override ARIA attributes unless absolutely necessary.
Best Practices
✅ Do:
Components:
- Use custom UI components from
@/components/uifirst - Fall back to Base UI for custom needs
- Combine Base UI primitives for complex UIs
- Leverage TypeScript for component props
Design:
- Follow spacing guidelines (powers of 2)
- Use
rounded-mdfor all border radius - Import icons with
Iconsuffix - Use default text sizes
- Use flex gap instead of space utilities
Forms:
- Use
UnsavedChangesBarfor edit forms - Use
Controllerfor controlled inputs - Validate on
onBlur - Reset form after successful save
❌ Don't:
Typography:
- ❌
text-sm,text-xs,text-lg,text-xl, etc.
Spacing:
- ❌
space-y-*,space-x-*utilities - ❌
gap-1,gap-3,gap-6,gap-12(not powers of 2)
Border Radius:
- ❌
rounded-lg,rounded-xl,rounded-full, etc.
Icons:
- ❌ Import without
Iconsuffix
Other:
- ❌ Override ARIA attributes
- ❌ Create custom implementations of Base UI components
- ❌ Use UnsavedChangesBar for create forms
Common Patterns
Item Lists
import { ItemGroup, Item, ItemContent, ItemTitle, ItemDescription } from '@/components/ui/item';
<ItemGroup className="gap-2">
{items.map(item => (
<Item key={item.id} className="px-0">
<ItemContent>
<ItemTitle>{item.title}</ItemTitle>
<ItemDescription>{item.description}</ItemDescription>
</ItemContent>
</Item>
))}
</ItemGroup>Consistent Spacing
// Page layout
<div className="flex flex-col gap-8">
<header>...</header>
<main>...</main>
<footer>...</footer>
</div>
// Form fields
<div className="flex flex-col gap-4">
<Field>...</Field>
<Field>...</Field>
<Field>...</Field>
</div>
// Horizontal layout
<div className="flex gap-2">
<Button>Cancel</Button>
<Button>Save</Button>
</div>Related Skills
form-patterns- Detailed form handling with React Hook Formtanstack-comprehensive- Data fetching and mutations for forms
Version: 1.0.0
Last updated: 2026-01-14