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>