| name | button-unification |
| description | Standardize button component heights, padding, and transitions across all variants. Ensures consistent appearance for default, outline, secondary, ghost, and link variants. Updates button.tsx with unified styles. |
Button Unification
Standardize the button component for consistent heights, padding, and transitions across all variants.
Workflow
- Find Button Component - Locate
website/components/ui/button.tsx - Audit Current Styles - Review existing buttonVariants
- Standardize Heights - Unified height per size
- Standardize Padding - Consistent padding per size
- Standardize Typography - Same font size and weight and variant
- Unify Border-Radius - Same radius for all variants
- Unify Transitions - Same transition for all variants
- Background - Ensure background styles are consistent for each variant
- Icon Sizes - Standardize icon button dimensions for each size
- Text Color - Ensure text colors are consistent per variant
- Verify Consistency - Test all variant + size combinations
Size Specifications
Heights (Unified)
| Size | Height | Class |
|---|---|---|
| default | 36px | h-9 |
| sm | 32px | h-8 |
| lg | 40px | h-10 |
| icon | 36px | size-9 |
| icon-sm | 32px | size-8 |
| icon-lg | 40px | size-10 |
Padding (Unified)
| Size | Horizontal | With Icon | Gap |
|---|---|---|---|
| default | px-4 | has-[>svg]:px-3 | gap-2 |
| sm | px-3 | has-[>svg]:px-2.5 | gap-1.5 |
| lg | px-6 | has-[>svg]:px-4 | gap-2 |
Base Styles (All Variants)
All button variants share these base styles:
// Base class (applied to ALL variants)
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm text-white font-medium transition-all"
Key unified properties:
rounded-md- Consistent border radiustext-sm font-medium text-white- Consistent typographytransition-all- Consistent transitionsgap-2(or gap-1.5 for sm) - Consistent spacing
Updated buttonVariants
See references/button-patterns.md for the complete CVA configuration.
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-white hover:bg-destructive/90",
outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 px-6 has-[>svg]:px-4",
icon: "size-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
Visual Consistency Check
After updating, verify:
- Same Height - Buttons in a row align perfectly
- Same Padding - Text has consistent spacing
- Same Radius - Corners match across variants
- Same Transition - Hover effects are smooth and consistent
- Same Font - Text appears identical size
- Icon Sizes - Icon buttons have correct dimensions
Button with Next.js Link
When a button needs to navigate, use the asChild prop with Next.js <Link>:
import Link from "next/link";
import { Button } from "@/components/ui/button";
// Correct pattern - Button wraps Link with asChild
<Button asChild>
<Link href="/login">Login</Link>
</Button>
<Button asChild variant="outline" size="sm">
<Link href="/contact">Contact</Link>
</Button>
<Button asChild variant="ghost" size="icon">
<Link href="/settings">
<Settings className="size-4" />
</Link>
</Button>
Key points:
- Always use
asChildprop when wrapping Link - Link is the child, Button provides styling
- Works with all variants and sizes
- Preserves Next.js client-side navigation
DO NOT do this:
// WRONG - Button onClick with router.push
<Button onClick={() => router.push('/login')}>Login</Button>
// WRONG - Link wrapping Button
<Link href="/login"><Button>Login</Button></Link>
Testing
Test all variant + size combinations:
// All variants at same size should have identical height
<div className="flex gap-2">
<Button variant="default">Default</Button>
<Button variant="outline">Outline</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
</div>
Checklist
- All sizes have unified heights (h-9, h-8, h-10)
- All sizes have unified padding (px-4, px-3, px-6)
- All variants use rounded-md
- All variants use transition-all
- All sizes use text-sm font-medium
- Icon sizes use square dimensions (size-9, size-8, size-10)
- Gap is consistent per size (gap-2, gap-1.5)
- Focus ring styles are consistent across variants
- Navigation buttons use
asChildwith<Link>