# UI_STANDARDS.md **Audience**: AI agents performing automated UI audits and refactors. **Scope**: Radix Primitives + Tailwind CSS (v4) + Responsive (Flexbox-first) with **Light** and **Dark** themes. **How to use**: Treat **Rules** as MUST, **Anti‑Patterns** as MUST NOT. Prefer the **Good Examples** verbatim unless there's a documented exception. --- ## 0) Project‑wide Conventions (applies to all sections) **Rules** - Use **no inline styles** (`style=""`) except when setting CSS variables (e.g., `style={{ "--accent": token }}`). Everything else must use Tailwind utilities or predeclared component classes. - Use a **single source of truth** for design tokens via Tailwind v4 `@theme` variables (colors, spacing, radii, shadows, breakpoints). Never invent ad‑hoc hex values or pixel sizes. - Keep markup **semantic and accessible**. Preserve focus outlines and keyboard navigation. - Prefer **wrapper components** (e.g., ` Title Description
``` ```css /* State-driven styling via data attributes */ .popover-content[data-state="open"] { /* animations, borders, etc. */ } ``` --- ## 2) Tailwind CSS (v4) **Rules** - **CRITICAL: Define custom dark variant FIRST** - Required for `dark:` utilities to work in v4. - **Define tokens properly**: Variables in `:root` and `.dark` with **bare HSL values** (NO hsl() wrapper), then map to Tailwind with `@theme inline`. ```css @import "tailwindcss"; @custom-variant dark (&:where(.dark, .dark *)); /* REQUIRED for dark: utilities */ :root { /* Bare HSL values - Tailwind adds hsl() wrapper automatically */ --background: 0 0% 98%; --foreground: 240 10% 3.9%; --border: 240 5.9% 90%; --radius: 0.5rem; color-scheme: light; } .dark { /* Bare HSL values - redefine same variables */ --background: 0 0% 0%; --foreground: 0 0% 100%; --border: 240 3.7% 15.9%; color-scheme: dark; } @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); --color-border: var(--border); --radius-lg: var(--radius); --radius-md: calc(var(--radius) - 2px); } ``` - **CRITICAL: NO dynamic class name construction**. Tailwind processes source code as **plain text at BUILD time**, not runtime. It cannot understand variables, string concatenation, or template literals. - **CSS variables ARE allowed in arbitrary values** as long as the utility name is static (e.g., `bg-[var(--accent)]`, `border-[color:var(--accent)]`). - **Use static class lookup objects** for discrete variants (e.g., `const colorClasses = { cyan: "bg-cyan-500 text-cyan-700", ... }`). - **Use CSS variables pattern** for flexible/continuous values (colors, alphas, glows, gradients). - **Conditional classes must be complete strings** (use `cn()` helper with full class names, not interpolated fragments). - **Arbitrary values are allowed** if written as complete static strings in source code (e.g., `shadow-[0_0_30px_rgba(34,211,238,0.4)]`). - **Inline style allowed ONLY for CSS variables** (e.g., `style={{ "--accent": token }}`), never for direct visual CSS. - **`@layer` and `@apply` are still valid** in v4 - use them for base styles and component classes. For custom utilities, register with `@utility` first. - **Dark mode**: use `dark:` variants consistently for colors, borders, shadows (and add `.dark` on ``). - **No arbitrary values** unless promoted to tokens or used as static one-offs; if you need a value repeatedly, add it to `@theme`. - **Reference tokens** for any custom CSS (e.g., `border-radius: var(--radius-md)`). - **If referencing vars inside vars**, use `@theme inline` to avoid resolution pitfalls. **Anti‑Patterns** - **Dynamic class construction**: `bg-${color}-500`, `` `text-${textColor}-700` ``, `className={isActive ? \`bg-${activeColor}-500\` : ...}` - CSS WILL NOT BE GENERATED. - **String interpolation for class names**: Template literals with variables, computed class names. - `style="..."` for visual styling (except setting local CSS vars). - Mixing utility classes that conflict or duplicate a property. - Introducing new colors/sizes ad‑hoc instead of defining in `:root`/`.dark` and mapping via `@theme inline`. **Good Examples** ### Pattern A: Lookup Map (discrete variants) ```tsx // ✅ CORRECT - Static class lookup object for finite set of variants const colorClasses = { cyan: "bg-cyan-500 text-cyan-700 border-cyan-500/50", purple: "bg-purple-500 text-purple-700 border-purple-500/50", blue: "bg-blue-500 text-blue-700 border-blue-500/50", };
// ✅ CORRECT - Conditional with complete class names
// ✅ CORRECT - Use cn() helper with complete class strings
``` ### Pattern B: CSS Variables (flexible values) ```tsx // ✅ CORRECT - CSS variables in arbitrary values (utility name is static) // In CSS: :root { --accent: oklch(0.75 0.12 210); }
// ✅ CORRECT - Set variable via inline style, use in static utilities
// ✅ CORRECT - Named accent classes that set CSS variables // In CSS: .accent-cyan { --accent: var(--color-cyan-500); } const accentClass = { cyan: "accent-cyan", orange: "accent-orange", } as const;
``` ### Anti-Patterns ```tsx // ❌ BROKEN - Dynamic class construction (CSS NOT GENERATED) const color = "cyan";
// NO CSS OUTPUT
// NO CSS OUTPUT // ❌ BROKEN - Inline style for visual CSS (use variables instead)
// Should be className or CSS variable // ✅ CORRECT alternative - Use CSS variable
``` ```tsx // Centralized Button component (preferred over scattered long class strings) export function Button({ variant = "primary", className = "", ...props }) { const base = "inline-flex items-center gap-2 rounded-[var(--radius-md)] px-3 py-2 text-sm font-medium " + "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 " + "transition-colors"; const variants = { primary: "bg-brand-500 text-white hover:brightness-110 dark:text-white " + "focus-visible:outline-brand-500", ghost: "bg-transparent text-fg dark:text-fg-dark hover:bg-black/5 dark:hover:bg-white/10" }; return ))}
// ❌ BROKEN - Native HTML elements ``` --- ## 6) Tailwind Tokens Quickstart (promote values to tokens) ```css @import "tailwindcss"; :root { --background: hsl(0 0% 98%); --foreground: hsl(240 10% 3.9%); --border: hsl(240 5.9% 90%); --brand-500: oklch(0.70 0.15 220); --radius: 0.5rem; color-scheme: light; } .dark { --background: hsl(0 0% 0%); --foreground: hsl(0 0% 100%); --border: hsl(240 3.7% 15.9%); color-scheme: dark; } @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); --color-border: var(--border); --color-brand-500: var(--brand-500); --radius-lg: var(--radius); --radius-md: calc(var(--radius) - 2px); } ``` Then use generated utilities (e.g., `bg-brand-500`, `border-border`) or token references (`bg-[var(--accent)]`). --- ## 7) UI Review Pre-Flight Checklist Before committing any UI component, verify ALL of these: ### Tailwind v4 Rules - [ ] **@custom-variant dark defined** (`@custom-variant dark (&:where(.dark, .dark *));` in index.css) - [ ] **No dynamic class construction** (`bg-${color}-500`, template literals with variables) - [ ] **CSS variables allowed in arbitrary values** (static utility names: `bg-[var(--accent)]`) - [ ] **Static class lookup objects** for discrete variants (e.g., `const colorClasses = { cyan: "...", ... }`) - [ ] **CSS variables pattern** for flexible/continuous values (colors, alphas, glows) - [ ] **Conditional classes are complete strings** (use `cn()` with full class names) - [ ] **Arbitrary values are static** (written as complete strings in source code) - [ ] **Inline style ONLY for CSS variables** (`style={{ "--accent": token }}`), never direct visual CSS - [ ] **Variables in `:root` and `.dark` with bare HSL values** (NO hsl() wrapper) - [ ] **Variables mapped with `@theme inline`** - [ ] **`@layer` and `@apply` used properly** (base styles and component classes) ### Responsive Layout Rules - [ ] **Desktop-primary with responsive adaptations** (optimize for desktop, add breakpoints for smaller screens) - [ ] **Horizontal scroll containers have `w-full` parent** (CRITICAL - prevents page-width expansion) - [ ] **Horizontal scroll containers use `scrollbar-hide`** (cleaner UI without visible scrollbar) - [ ] **Flex parent containing scroll container has `min-w-0`** (CRITICAL - prevents page expansion) - [ ] **Responsive grid columns** (`grid-cols-1 md:grid-cols-2 lg:grid-cols-3` etc.) - [ ] **Flexbox adapts at breakpoints** (`flex-col md:flex-row`) - [ ] **Text has truncation/wrapping** (`truncate`, `line-clamp-N`, or `break-words max-w-full`) - [ ] **Fixed widths have `max-w-*` constraints** (no bare `w-[500px]`) - [ ] **Add `min-w-0` to flex children** that contain text/images (allows shrinking) - [ ] **Layout toggle buttons always visible** (not absolutely positioned off-screen) - [ ] **Use `min-h-[100dvh]`** instead of rigid `h-screen` on mobile - [ ] **Remove layout-level `overflow-hidden`** (clips popovers/toasts/dialogs) ### Component Reusability Rules - [ ] **Use Card primitive** for glassmorphism/edge-lit effects (no hardcoded patterns) - [ ] **Use PillNavigation component** for tab navigation (no custom implementations) - [ ] **Use Radix UI primitives** for all interactive elements (Select, Checkbox, RadioGroup, Dialog, etc.) - [ ] **Import from styles.ts** for shared class objects when appropriate - [ ] **No duplicated styling patterns** (DRY - create wrapper components if repeated 3+ times) ### Radix Primitives Rules - [ ] **Use `asChild` for custom triggers** (compose, don't wrap) - [ ] **Follow documented structure** (Root/Trigger/Portal/Overlay/Content/Close) - [ ] **Style via data attributes** (`[data-state="open"]`, `[data-disabled]`) - [ ] **Use Portal and z-index scale** for overlays (no random `z-[9999]`) - [ ] **Never remove focus rings or ARIA** attributes ### Light/Dark Theme Rules - [ ] **Every color/border/shadow has `dark:` variant** where visible - [ ] **Structure identical between themes** (only colors/opacity/shadows change) - [ ] **Use tokens for both themes** (`--color-bg` and `--color-bg-dark`) ### Testing Requirements - [ ] **Tested at 375px width** (mobile - iPhone SE) - [ ] **Tested at 768px width** (tablet) - [ ] **Tested at 1024px width** (laptop) - [ ] **Tested at 1440px+ width** (desktop) - [ ] **All UI controls accessible** at all sizes - [ ] **No unintended horizontal page scroll** (only intentional container scroll) - [ ] **Images scale responsively** - [ ] **Modals/dialogs fit within viewport** - [ ] **Touch targets minimum 44x44px** on mobile ### Common Violations to Fix Immediately - ❌ `bg-${color}-500` → ✅ `colorClasses[color]` - ❌ `grid-cols-4` → ✅ `grid-cols-1 md:grid-cols-2 lg:grid-cols-4` (Symptom: page scrolls horizontally, controls off-screen) - ❌ Hardcoded glassmorphism → ✅ `` - ❌ `` from Radix - ❌ Custom pill navigation → ✅ `` - ❌ `overflow-x-auto` without `w-full` parent → ✅ Wrap in `
` - ❌ Long text with no truncation → ✅ Add `truncate` or `line-clamp-N` - ❌ Fixed `w-[500px]` → ✅ `w-full max-w-[500px]` - ❌ Absolute positioned controls → ✅ Use flexbox layout - ❌ No dark mode variant → ✅ Add `dark:` classes --- ## 8) Automated Scanning Patterns Use these patterns to programmatically detect violations in `.tsx` files. ### Critical Violations (Breaking Changes) **Missing @custom-variant dark (DARK MODE WON'T WORK)** ```bash grep -n "@custom-variant dark" [path]/index.css ``` **Rule**: Section 2 - @custom-variant dark REQUIRED for Tailwind v4 **Symptom**: dark: utilities don't apply, theme toggle appears to work but nothing changes visually **Fix**: Add `@custom-variant dark (&:where(.dark, .dark *));` immediately after `@import "tailwindcss";` **Dynamic Tailwind Class Construction (NO CSS GENERATED)** ```bash grep -rn "className={\`.*\${.*}.*\`}" [path] --include="*.tsx" grep -rn "bg-\${.*}\|text-\${.*}\|border-\${.*}\|shadow-\${.*}" [path] --include="*.tsx" ``` **Rule**: Section 2 - NO dynamic class name construction **Fix**: Use static class lookup objects (Pattern A from Section 2) **Unconstrained Horizontal Scroll (BREAKS LAYOUT)** ```bash # Find overflow-x-auto without w-full parent grep -rn "overflow-x-auto" [path] --include="*.tsx" # Then manually verify parent has w-full or max-w-* ``` **Rule**: Section 3 - Constrain horizontal scroll containers **Fix**: Wrap in `
` (Example in Section 3) **Non-Responsive Grid Columns (BREAKS MOBILE)** ```bash grep -rn "grid-cols-[2-9]" [path] --include="*.tsx" | grep -v "md:\|lg:\|sm:\|xl:" ``` **Rule**: Section 3 - Responsive grid columns mandatory **Symptom**: Page scrolls horizontally, UI controls (grid/list toggles, navigation buttons) shift off-screen and become inaccessible **Fix**: Add responsive breakpoints: `grid-cols-1 md:grid-cols-2 lg:grid-cols-4` (Section 3) **Min-w-max Without Constraint** ```bash grep -rn "min-w-max" [path] --include="*.tsx" ``` **Rule**: Section 3 - min-w-max must have constrained parent **Fix**: Ensure parent has `w-full` or `max-w-*` ### High Priority Violations **Native HTML Form Elements (Should Use Radix)** ```bash grep -rn "