Rules for AI agents and contributors editing this codebase. Follow every rule. No exceptions on docs.
- Use latest stable versions of packages. Ask if unsure.
- Run
bun format && bun typecheckafter changes. Fix errors before completing. - Run
bunx react-doctor .after significant changes to catch performance, security, correctness, and dead code issues. Fix errors (score 75+ = healthy). - Docs are mandatory — README is the source of truth. Incomplete doc updates = task incomplete. Never leave docs stale.
- Avoid deprecated APIs — Check for deprecation warnings, use recommended replacements. If a package marks an API deprecated, find the new import path or alternative.
- Don't bloat the codebase.
Rule: When you add, remove, or change a component or block, you MUST update README in the same change. Do not defer. Incomplete doc updates = task incomplete.
Docs: README only. Components table, blocks table, API Reference, Flow Patterns, Quick Reference.
Cross-references: Fix broken links.
- Export from
packages/ui/src/components/<name>/index.tsandpackages/ui/src/index.ts - Add to
README.mdcomponents table - Add to README API Reference if notable props
- Add showcase demo in
apps/showcase/App.tsx - Add entry to
apps/site/lib/registry.ts(components array) - Add live demo in
apps/site/lib/demos.tsx - Add props data in
apps/site/lib/props-data.ts
Do not skip steps 2–7. Missing doc, showcase, or site entry = incomplete.
- Export from
packages/ui/src/blocks/<category>/index.tsandpackages/ui/src/index.ts - Add to
README.mdblocks table - Add to README Blocks API section with key props
- Add showcase demo in
apps/showcase/App.tsx - Add entry to
apps/site/lib/registry.ts(blocks array, setwebSupport) - If
webSupport: 'full': add live demo inapps/site/lib/block-demos.tsx - If new flow: add to README Flow Patterns table
Do not skip steps 2–6. Missing doc, showcase, or site entry = incomplete.
- Props: Update README API section, Blocks API,
apps/site/lib/props-data.ts, and demo code - Removing: Remove from README (all sections), showcase, and site (
registry.ts,demos.tsx/block-demos.tsx,props-data.ts). No orphan references
global.cssat app root (notsrc/) — Tailwind scans from its locationwithUniwindConfigmust be outermost wrapper inmetro.config.js- Import
global.cssinApp.tsx(notindex.ts) for HMR cssEntryFilemust be relative path string ('./global.css')- Monorepos: use
@source '../../packages/ui/src';in CSS - Docs: https://docs.uniwind.dev/llms-full.txt
- Use
useUniwind()+Uniwind.setTheme()— NOT local React state - Components use semantic classes (
bg-background,text-foreground) — no theme code in UI library - Custom themes: register in
metro.config.jswithextraThemes, define inglobal.cssusing@variant - OKLCH color format: Use
oklch(L C H)for all color values (better perceptual uniformity) - Theme colors MUST be wrapped in
@layer theme { :root { @variant light/dark { ... } } }
All visual properties are controlled via design tokens. To redesign the theme, modify global.css only — no component changes needed.
| Token Category | Example Variables | Controls |
|---|---|---|
| Spacing | --spacing, --spacing-1 through --spacing-96 |
p-*, m-*, gap-*, w-*, h-* |
| Typography | --text-xs through --text-6xl |
text-sm, text-base, text-xl, etc. |
| Letter Spacing | --tracking-tighter through --tracking-widest |
tracking-tight, tracking-wide |
| Line Height | --leading-none through --leading-loose |
leading-none, leading-tight |
| Border Radius | --radius-none through --radius-full |
rounded-sm, rounded-lg, rounded-xl |
| Shadows | --shadow-sm through --shadow-2xl, --shadow-none |
shadow-sm, shadow-lg, shadow-xl |
| Font | --font-sans |
Base font family |
Required in both light and dark (and any custom theme):
background,foreground,card,primary,secondary,muted,accent,destructive,border,input,ringsuccess,warning,info(status colors)- Each token needs
*-foregroundvariant for text on that background
React Native requires explicit font-family per weight. When changing fonts, update BOTH --font-sans in @theme AND the .font-* CSS classes in global.css.
Apps using custom fonts must wrap the app in FontProvider with a FontFamilyMap (see font-context.ts). Text, ButtonText, and Label use useResolveFontFamily() to apply the correct font via the style prop and strip font-weight classes from className so Uniwind doesn't override. Do not pass fontWeight in the style—with discrete font files (e.g. Nunito_700Bold), the family name encodes weight. Passing fontWeight on Android triggers synthetic bold and distorts B, D, P, R.
All components use tailwind-variants for styling:
const buttonVariants = tv({
base: 'rounded-md font-medium',
variants: {
variant: { default: 'bg-primary', outline: 'border' },
size: { sm: 'h-8', md: 'h-10' },
},
defaultVariants: { variant: 'default', size: 'md' },
});Export *Variants for consumers who want to extend styles.
Components accepting children must handle both strings and elements:
{
typeof children === 'string' ? <Text className="...">{children}</Text> : children;
}Applies to: AvatarFallback, DropdownMenuLabel, AlertDialogAction/Cancel, etc.
Parent components (Button, Badge, Toggle) provide Context so child text components auto-inherit variant:
const ButtonContext = createContext({ variant: 'default', size: 'default' });
// ButtonText uses useContext(ButtonContext)cloneElementdoesn't work with complex components (Context.Provider wrappers)- Don't use
asChildwithView— it lacksonPress/onLongPress - Prefer wrapping children in Pressable over cloneElement injection
- SafeAreaView: use
react-native-safe-area-context(RN's deprecated) - CSS box-shadow: Not supported. Use
shadow-sm/md/lgclasses (soft shadows) or stacked Views for hard-edge 3D effects (NeoPOP style) - Icon colors:
@expo/vector-iconsrequires hex values, not CSS classes. UseuseIconColors()from the UI package (reads from global.css viauseCSSVariable) - Other RN primitives requiring hex: Spinner defaults to
useThemeColors().foreground. Input, Textarea, NumericInput, SmartInput defaultplaceholderTextColortomutedForeground. Add optional override props when needed. - Custom fonts on Android: Pass only
fontFamilyin style—neverfontWeight. Passing both triggerssetTypeface(_, BOLD)and synthetic bold, distorting B/D/P/R. UseFontProvider+useResolveFontFamily(Text, ButtonText, Label).
WhileUI targets both native apps and web. Components should be web-aware by default where behavior differs:
- Overlays (DrawerMenu, modals, sheets): Use sensible web defaults (e.g. max-width ~360px for drawers). Add optional
maxWidthorwidthprops for override. - Platform.OS: Use for behavior that truly differs (haptics, native APIs). Guard web-incompatible code (
if (Platform.OS === 'web') return). - useWindowDimensions: Use for layout breakpoints when width matters (e.g.
width >= 768for desktop layout). - Override props: Provide
maxWidth,width, etc. so apps can customize. Library has smart defaults; apps opt in to overrides. - Uniwind
web:variant: Use for platform-specific styling in apps; library may usePlatform.OSfor structural behavior.
- Overlays (Dialog, Popover, Menus):
rounded-lg - Controls (Button, Input, Select):
h-10 - Cards:
rounded-xl
- Minimum 44px for mobile touch targets — even if visually smaller
- Icon-only buttons:
h-11 w-11(44px) - Small buttons:
h-9minimum (36px visual, usehitSlopif needed)
- Never toggle font-weight for active/inactive states — causes width jump
- Use color-only differentiation (e.g.,
text-primaryvstext-muted-foreground) - Keep
font-mediumconstant across states
- Always provide tactile feedback on interactive elements
- Buttons:
active:bg-*/90(slight darken) - Nav items:
active:opacity-70 - Floating elements:
active:scale-95
Add to this file when you discover:
- Configuration gotchas (one-liner under relevant section)
- Reusable patterns (with minimal code example)
- Root causes of bugs (symptom → cause → fix)
Keep entries concise. Consolidate similar issues. Delete outdated rules.
- Android font B/D distorted: Symptom: bold/semibold custom fonts render B, D, P, R incorrectly on Android only. Cause: passing
fontWeightwithfontFamilytriggers synthetic bold. Fix: useFontProvider+useResolveFontFamily(applies onlyfontFamilyvia style, strips font-weight fromclassName). - Text/label line-height: Avoid
leading-none— clips ascenders (P, h, l). Useleading-tightorleading-snugfor labels. - Form-like visibility: Use
border-border bg-muted(notborder-input bg-background) for inputs, selects, labeled fields — ensures visibility on light themes. - Small touch targets: Components under 44px (e.g. Checkbox h-5, Radio h-5, Switch h-7) need
hitSlopso effective touch area ≥ 44px. - Browser focus outline on inputs: Always add
outline-nonetoTextInputclassName. Without it, browsers render a blue/black outline on focus that doubles up with the component'sborder-border.
- Blocks = copy-paste compositions. No themeability props needed — people edit the block code directly. Use semantic tokens; theme via
global.css. - Components = imported dependencies. Must be themeable (semantic classes,
*Variantsfor extension).
| What | Path | Notes |
|---|---|---|
| Component | packages/ui/src/components/<name>/ |
Primitives, controls, display. Export from index.ts, re-export in packages/ui/src/index.ts |
| Block (core) | packages/ui/src/blocks/<category>/ |
Categories: layout, navigation, chat, lists, commerce, media, datepicker, splash |
| Auth/Profile | apps/showcase/templates/auth/ or profile/ |
Copy-paste templates; NOT in core package. Import primitives from @thewhileloop/whileui |
| Shared hook/util | packages/ui/src/lib/ |
Theme helpers, cn, portal, tv, font-context |
| Site registry | apps/site/lib/registry.ts |
Metadata for all components and blocks (slug, name, category, description, webSupport) |
| Site demos | apps/site/lib/demos.tsx |
Live previews + code snippets for components. Every component must have one |
| Site block demos | apps/site/lib/block-demos.tsx |
Live previews + code snippets for web-supported blocks |
| Site props | apps/site/lib/props-data.ts |
API reference data (prop name, type, default, description) per component |
Create reusable theme presets that can be switched at runtime:
// metro.config.js
module.exports = withUniwindConfig(config, {
cssEntryFile: './global.css',
extraThemes: ['noir', 'minimalist', 'brand-accent'],
});@layer theme {
:root.noir,
.noir {
@variant light {
--color-background: oklch(1 0 0);
--color-foreground: oklch(0 0 0);
--color-primary: oklch(0 0 0);
/* ... other variables */
--radius-sm: 0px;
--radius-md: 0px;
}
@variant dark {
/* dark mode variables */
}
}
}import { Uniwind } from 'uniwind';
// Switch to custom theme
Uniwind.setTheme('noir');
// Switch back to default light/dark
Uniwind.setTheme('light');
Uniwind.setTheme('dark');Apps that want a frosted or translucent look for floating panels (modals, sheets, toolbars) can override surface tokens in their theme with semi-transparent values, e.g. --color-surface-elevated: oklch(0.98 0.01 95 / 0.4). Optional tokens: surface-translucent, surface-translucent-border. No "glass" in core names. See README Theming > Frosted / Translucent Theme.
All themes must define these variables for components to work:
--color-background,--color-foreground--color-card,--color-card-foreground--color-primary,--color-primary-foreground--color-secondary,--color-secondary-foreground--color-accent,--color-accent-foreground--color-muted,--color-muted-foreground--color-destructive,--color-destructive-foreground--color-border,--color-input,--color-ring--color-success,--color-success-foreground--color-warning,--color-warning-foreground--color-info,--color-info-foreground--radius-sm,--radius-md,--radius-lg,--radius-xl