Saltar al contenido

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.

4 min de lectura
Biblioteca de componentes de un sistema de diseño mostrando tokens, variantes y patrones de composición

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.

tstypescript
// 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.

tsxtsx
// ❌ 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.

tstypescript
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.

tsxtsx
// 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:

tstypescript
// 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

  1. 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
  2. Prefiere props de composición sobre props de funcionalidad — asChild, children y la propagación de atributos HTML sobreviven a la proliferación de props
  3. cva hace 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
  4. Los componentes compuestos codifican patrones de interacción — los equipos deberían consumir comportamientos, no reimplementar trampas de foco y ARIA
  5. La documentación es una funcionalidad del producto — los componentes sin documentar se reimplementan; las stories interactivas de Storybook son el mínimo exigible
Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX