You don't need a full design system on day one. These 4 components — Button, Input, Card, Badge — cover 90% of a SaaS dashboard UI. Here they are, built with Tailwind CSS v4 and React.
1. Button
A button with 5 variants, 3 sizes, and a loading state:
import { forwardRef, ButtonHTMLAttributes } from "react";
import { cn } from "@/lib/utils"; // clsx + tailwind-merge
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary" | "ghost" | "danger" | "outline";
size?: "sm" | "md" | "lg";
loading?: boolean;
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = "primary", size = "md", loading, children, disabled, ...props }, ref) => (
<button
ref={ref}
disabled={disabled || loading}
className={cn(
"inline-flex items-center justify-center font-medium rounded-lg transition-all active:scale-[0.98] disabled:opacity-50 disabled:pointer-events-none",
{
"bg-white text-zinc-900 hover:bg-zinc-100": variant === "primary",
"bg-zinc-800 text-zinc-100 hover:bg-zinc-700 border border-zinc-700": variant === "secondary",
"text-zinc-400 hover:text-zinc-100 hover:bg-zinc-800/50": variant === "ghost",
"bg-red-600 text-white hover:bg-red-500": variant === "danger",
"border border-zinc-700 text-zinc-300 hover:bg-zinc-800": variant === "outline",
},
{
"h-8 px-3 text-xs": size === "sm",
"h-10 px-4 text-sm": size === "md",
"h-12 px-6 text-base": size === "lg",
},
className,
)}
{...props}
>
{loading && (
<svg className="animate-spin h-4 w-4 mr-2" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
)}
{children}
</button>
),
);
2. Input
With label, error state, and proper focus rings:
export const Input = forwardRef<HTMLInputElement, InputProps>(
({ className, label, error, id, ...props }, ref) => (
<div className="space-y-1.5">
{label && <label htmlFor={id} className="text-sm font-medium text-zinc-300">{label}</label>}
<input
ref={ref}
id={id}
className={cn(
"flex h-10 w-full rounded-lg border bg-zinc-900 px-3 py-2 text-sm text-zinc-100",
"placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50",
error ? "border-red-500" : "border-zinc-700",
className,
)}
{...props}
/>
{error && <p className="text-xs text-red-400">{error}</p>}
</div>
),
);
3. Card
Composable card with header, title, and description:
export function Card({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
return <div className={cn("rounded-xl border border-zinc-800 bg-zinc-900/50 p-6", className)} {...props} />;
}
export function CardTitle({ className, ...props }: HTMLAttributes<HTMLHeadingElement>) {
return <h3 className={cn("text-lg font-semibold text-zinc-100", className)} {...props} />;
}
export function CardDescription({ className, ...props }: HTMLAttributes<HTMLParagraphElement>) {
return <p className={cn("text-sm text-zinc-400", className)} {...props} />;
}
4. Badge
5 color variants for status indicators:
export function Badge({ className, variant = "default", ...props }: BadgeProps) {
return (
<span
className={cn(
"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium",
{
"bg-zinc-800 text-zinc-300": variant === "default",
"bg-green-500/20 text-green-400": variant === "success",
"bg-yellow-500/20 text-yellow-400": variant === "warning",
"bg-red-500/20 text-red-400": variant === "danger",
"bg-blue-500/20 text-blue-400": variant === "info",
},
className,
)}
{...props}
/>
);
}
The cn() utility
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
clsx handles conditional classes. tailwind-merge resolves Tailwind conflicts. Together they let you compose className props without worrying about specificity.
Why these 4 are enough
- Button — actions, forms, navigation
- Input — forms, search, filters
- Card — content containers, stats, settings sections
- Badge — status indicators, labels, counts
With variants, these cover dashboards, settings pages, billing pages, and landing pages. Add more components when you actually need them.
Want the full set?
These components (plus auth, Stripe billing, AI chat, and a complete SaaS dashboard) come pre-built in LaunchKit.
Top comments (0)