Construyendo un sistema de diseño: de los tokens a los componentes
Tokens de diseño, APIs de componentes y los patrones que convierten un sistema de diseño en un multiplicador de fuerza y no en una carga de mantenimiento.

La mayoría de los sistemas de diseño empiezan como una biblioteca de componentes y terminan como un cuello de botella. Los equipos copian componentes en sus propios repositorios para ir más rápido, la biblioteca se desincroniza, y acabas con tres botones ligeramente distintos repartidos entre cuatro productos. El problema rara vez es técnico — es arquitectónico. Sentar bien las bases desde el principio evita esa deriva.
Empieza por los tokens, no por los componentes
Los tokens de diseño son la capa atómica: valores con nombre para color, espaciado, tipografía y radio que viven en un solo lugar y fluyen hacia afuera. Construir componentes antes de establecer los tokens significa que acabarás con valores hardcodeados por todas partes y refactorizando para siempre.
// tokens.ts — the single source of truth
export const tokens = {
color: {
brand: {
50: "#eff6ff",
500: "#3b82f6",
600: "#2563eb",
900: "#1e3a8a",
},
semantic: {
primary: "var(--color-brand-500)",
danger: "var(--color-red-500)",
success: "var(--color-green-500)",
warning: "var(--color-yellow-500)",
},
},
spacing: {
1: "0.25rem",
2: "0.5rem",
4: "1rem",
8: "2rem",
16: "4rem",
},
radius: {
sm: "0.25rem",
md: "0.375rem",
lg: "0.5rem",
full: "9999px",
},
} as const;Genera propiedades personalizadas de CSS a partir de estos tokens como parte de tu build. Cada componente referencia var(--color-brand-500), no #3b82f6. Cambiar de tema se convierte en un intercambio de variables, no en un grep-and-replace.
Diseñando APIs de componentes que envejecen bien
El mayor coste de mantenimiento de un sistema de diseño es el vaivén constante de las APIs de los componentes. Las props añadidas a la ligera se vuelven permanentes — nunca puedes eliminarlas sin romper a los consumidores. Diseña las APIs como si nunca fueras a poder cambiarlas.
// ❌ Prop explosion — every feature request adds a prop
interface ButtonProps {
label: string;
isLoading: boolean;
isDisabled: boolean;
isFullWidth: boolean;
iconLeft?: React.ReactNode;
iconRight?: React.ReactNode;
loadingText?: string;
onClick: () => void;
}
// ✅ Composition-first — consumers control content, system controls style
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary" | "ghost" | "danger";
size?: "sm" | "md" | "lg";
loading?: boolean;
asChild?: boolean; // Radix UI pattern for polymorphic rendering
}
function Button({
variant = "primary",
size = "md",
loading,
asChild,
children,
...props
}: ButtonProps) {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={buttonVariants({ variant, size })}
disabled={loading || props.disabled}
aria-busy={loading}
{...props}
>
{loading && <Spinner aria-hidden />}
{children}
</Comp>
);
}El patrón asChild (tomado de Radix UI) permite a los consumidores renderizar los estilos del botón sobre cualquier elemento — un enlace, un Link del router, un div — sin necesitar una variante separada ButtonLink.
Gestión de variantes con Class Variance Authority
Mantener los estilos de las variantes como cadenas de clases condicionales se vuelve inmanejable rápidamente. cva (Class Variance Authority) convierte las variantes en un sistema estructurado y con tipado seguro.
import { cva, type VariantProps } from "class-variance-authority";
export const buttonVariants = cva(
// Base styles — always applied
"inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
primary:
"bg-brand-500 text-white hover:bg-brand-600 active:bg-brand-700",
secondary: "border border-brand-500 text-brand-500 hover:bg-brand-50",
ghost: "text-brand-500 hover:bg-brand-50",
danger: "bg-red-500 text-white hover:bg-red-600",
},
size: {
sm: "h-8 px-3 text-sm rounded-md",
md: "h-10 px-4 text-sm rounded-lg",
lg: "h-12 px-6 text-base rounded-lg",
},
},
defaultVariants: {
variant: "primary",
size: "md",
},
},
);
// Type-safe — TypeScript knows which variants exist
type ButtonVariants = VariantProps<typeof buttonVariants>;cva devuelve una función que retorna una cadena de clases. Las variantes son enumerables, documentadas y verificadas por tipos. Añadir una variante es una entrada más en la configuración, en lugar de un condicional disperso por ahí.
Patrones de composición de componentes
Un sistema de diseño no es solo un conjunto de átomos. El verdadero valor está en los componentes compuestos que codifican patrones de interacción complejos — cosas como menús desplegables, diálogos y menús de comandos que los equipos, de otro modo, implementarían de forma distinta cada vez.
// Compound component pattern: shared state, composable structure
const Dialog = {
Root: DialogRoot,
Trigger: DialogTrigger,
Content: DialogContent,
Title: DialogTitle,
Close: DialogClose,
};
// Usage — flexible structure, consistent behavior
function ConfirmDialog({ onConfirm }: { onConfirm: () => void }) {
return (
<Dialog.Root>
<Dialog.Trigger asChild>
<Button variant="danger">Delete Account</Button>
</Dialog.Trigger>
<Dialog.Content>
<Dialog.Title>Are you sure?</Dialog.Title>
<p>This action is permanent and cannot be undone.</p>
<div className="flex gap-2 justify-end mt-4">
<Dialog.Close asChild>
<Button variant="ghost">Cancel</Button>
</Dialog.Close>
<Button variant="danger" onClick={onConfirm}>
Delete
</Button>
</div>
</Dialog.Content>
</Dialog.Root>
);
}El patrón de componentes compuestos mantiene los detalles de implementación (gestión del foco, atributos ARIA, animación) dentro del sistema, mientras deja la estructura y el contenido en manos de los consumidores.
La documentación como ciudadano de primera clase
Un componente que no está documentado no existe. Los equipos lo reimplementarán. Todo componente necesita tres cosas en su documentación: cuándo usarlo, cuándo no usarlo y ejemplos interactivos para cada variante relevante.
Storybook con documentación autogenerada a partir de los tipos de TypeScript es el estándar actual. La configuración clave es la etiqueta autodocs y la inferencia de argTypes:
// Button.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "./Button";
const meta: Meta<typeof Button> = {
component: Button,
tags: ["autodocs"], // generates a docs page automatically
argTypes: {
variant: { control: "select" },
size: { control: "radio" },
loading: { control: "boolean" },
},
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = { args: { children: "Get started" } };
export const Loading: Story = {
args: { children: "Saving...", loading: true },
};
export const Danger: Story = {
args: { children: "Delete", variant: "danger" },
};Storybook se convierte en la documentación de tu contrato. Las pruebas de regresión visual (Chromatic, Percy) evitan regresiones de estilo a medida que la biblioteca evoluciona.
Conclusiones clave
- Establece los tokens de diseño antes de escribir cualquier componente — los valores semánticos con nombre son lo que hace posible la tematización y la consistencia a escala
- Prefiere props de composición sobre props de funcionalidad —
asChild,childreny la propagación de atributos HTML sobreviven a la proliferación de props cvahace que la gestión de variantes sea segura en tipos y auditable — las variantes se convierten en una configuración estructurada, no en condicionales dispersos- Los componentes compuestos codifican patrones de interacción — los equipos deberían consumir comportamientos, no reimplementar trampas de foco y ARIA
- La documentación es una funcionalidad del producto — los componentes sin documentar se reimplementan; las stories interactivas de Storybook son el mínimo exigible


