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.
Complete guide to the project's design system using Base UI components and strict design guidelines.
This skill covers:
@/components/uiReference these guidelines when:
| 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 |
@base-ui/react # Unstyled primitives
ā
@/components/ui # Custom styled components
ā
Page components # App-specific usage
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>
Complete documentation with examples:
references/components.md - Base UI components, custom UI, form patterns, accessibilityTo find specific patterns:
grep -l "Dialog" references/*.md
grep -l "UnsavedChangesBar" references/*.md
grep -l "spacing" references/*.md
ā 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.
ā 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)
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>
ā 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.
When to use: Modifying existing data (profiles, settings, configurations)
Key features:
onBlur for fields, onSubmit for formimport { 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:
useFormState({ control: form.control }) for reactive isDirtyController for controlled inputsField, FieldLabel, FieldError componentsmode: 'onBlur' for field validationform.reset(value) after successful saveWhen to use: Creating new entities (API keys, invitations, new organizations)
Key features:
onBlurfunction 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>
);
}
| 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:
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.
Base UI provides:
Don't override ARIA attributes unless absolutely necessary.
Components:
@/components/ui firstDesign:
rounded-md for all border radiusIcon suffixForms:
UnsavedChangesBar for edit formsController for controlled inputsonBlurTypography:
text-sm, text-xs, text-lg, text-xl, etc.Spacing:
space-y-*, space-x-* utilitiesgap-1, gap-3, gap-6, gap-12 (not powers of 2)Border Radius:
rounded-lg, rounded-xl, rounded-full, etc.Icons:
Icon suffixOther:
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>
// 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>
form-patterns - Detailed form handling with React Hook Formtanstack-comprehensive - Data fetching and mutations for formsVersion: 1.0.0 Last updated: 2026-01-14