DiseñoAPI de componentes: 6 reglas de props que evitan el mal uso
Cuatro props booleanas dan 16 versiones del mismo botón. Seis reglas para APIs de componentes en las que el uso incorrecto no compila, con el TypeScript.

Diseñar la API de un componente es elegir sus props de forma que el uso previsto sea el más corto de escribir y el incorrecto se quede por el camino antes de llegar a pantalla. En las props una decisión de diseño deja de ser un dibujo y se convierte en una regla: un archivo de Figma muestra que un botón tiene tres variantes, pero solo la firma del tipo puede rechazar una cuarta.
Quien mantiene un mismo design system en varios productos se topa con este problema antes que nadie. Cada prop que acepta una combinación que nadie dibujó es una combinación que alguien acabará publicando. Aquí van las seis reglas que aplicamos a las props, un ejemplo completo sobre un botón y lo que cuesta una API estricta.
¿Por qué sale tan cara una API de componentes demasiado permisiva?
Pongamos un botón con cuatro booleanos opcionales: primary, secondary, danger y small. Cuatro flags independientes dan 16 combinaciones. El archivo de diseño muestra unas pocas. El resto no está prohibido en ningún sitio que el compilador pueda ver, así que llegan a producción con pull requests normales: un botón primario y secundario a la vez, que se queda con la clase que gane en la cascada, o una acción destructiva dibujada como un enlace discreto.
Ningún test las detecta, porque nada falla. Aparecen como deriva: la misma acción dibujada de tres maneras en tres pantallas. Contamos cómo se acumula en el artículo sobre component drift. Casi siempre entra por las props.
¿Por qué no basta con la documentación?
La respuesta de siempre es una página de uso que dice "no combines primary y secondary". La página tiene razón y cambia poco, porque quien desarrolla lee la firma del tipo en el editor, no la web de la documentación. Cuando documentación y tipo no coinciden, gana el tipo cada vez que alguien tira del autocompletado. La regla tiene que pasar al tipo.
Regla 1: una sola prop de variante en lugar de booleanos que compiten
Si varias props describen aspectos que se excluyen entre sí, en realidad son una sola prop con varios valores. La guía de diseño de API de MUI compara las dos formas y enumera lo que aporta el enum: impide combinaciones no válidas, limita el número de props expuestas y admite valores nuevos sin props nuevas. El precio son unos pocos caracteres: variant="primary" en lugar de primary.
La comprobación lleva un segundo. Si dos booleanos activos a la vez no tienen sentido visual, nunca fueron independientes. Los booleanos se quedan para los estados que se combinan con todo lo demás, como disabled o loading.
Regla 2: las props que dependen entre sí, en una discriminated union
Algunas props solo tienen sentido juntas. Un botón que lleva a otra página necesita un href y ningún manejador de clic. Un botón que ejecuta una acción necesita un onClick y ningún href. Con dos props opcionales valen "las dos" y "ninguna". Con una discriminated union de TypeScript, cada rama declara lo que exige y lo que prohíbe, y dentro del componente el compilador acota el tipo.
Yaron Minsky, de Jane Street, resumió el principio en cuatro palabras: "make illegal states unrepresentable", es decir, que los estados no válidos no se puedan ni escribir. En las props, esos estados son las combinaciones sobre las que de otro modo avisarías en un comentario del código.
Regla 3: hacer obligatorio lo que la accesibilidad exige
Un botón que solo lleva icono no tiene texto visible, así que no tiene nombre accesible hasta que quien lo usa le da uno. Primer, el design system de GitHub, deja esa tarea a quien llama al componente: sus notas de accesibilidad de IconButton piden un aria-label lo bastante descriptivo, que pasa a ser el nombre accesible del botón. Si la prop es obligatoria en el tipo, una etiqueta que falta se convierte en un error de compilación en el ordenador de quien desarrolla, semanas antes de que la encuentre una auditoría. Lo mismo vale para el texto alternativo de una imagen, el título de un diálogo y la etiqueta de un campo.
Regla 4: las props se nombran por su función, nunca por su aspecto
color="red" describe un píxel. tone="danger" describe una decisión, y sigue valiendo después del rebranding que vuelve naranja el peligro. Es la misma separación que usamos en los nombres de los design tokens: la prop habla el vocabulario semántico y los tokens de detrás guardan los valores. Y una misma idea lleva un solo nombre en toda la librería. Si un componente acepta disabled, ninguno debería aceptar isEnabled.
Regla 5: cerrar la vía de escape de los estilos, o declararla como tal
Una prop style o className abierta en todos los componentes es un segundo design system sin reglas. Cada override que permite queda fuera de la vista de quien mantiene el sistema hasta que cambia algo por debajo. Dos sistemas muy usados muestran las dos salidas.
- React Spectrum, de Adobe, le pone nombre. Sus vías de escape se llaman
UNSAFE_classNameyUNSAFE_style, y su guía de estilos desaconseja con firmeza sobrescribir los estilos de Spectrum y propone crear un componente propio. Cada override se encuentra con una búsqueda. - Primer, de GitHub, quitó una. En Primer React v38, anunciado en octubre de 2025, la prop
sxdeja de estar soportada y los estilos pasan porclassNamey CSS Modules. GitHub publicó además un paquete temporal,@primer/styled-react, para los componentes que todavía usabansxen otros proyectos: una medida de cuánto código acumula en pocos años una sola prop de estilo abierta.
Elijas la salida que elijas, cuenta los usos. Si los overrides crecen en cada release, te están diciendo qué variante le falta al sistema.
Regla 6: componer con hijos en lugar de añadir una prop por caso
Las props se multiplican cuando un componente quiere resolver todos los layouts: iconLeft, iconRight, badge, as, linkComponent. La composición lleva esa variación a los hijos. Con asChild de Radix, una parte no pinta su elemento por defecto y pasa sus props y su comportamiento al hijo, así que el trigger de un menú puede ser el enlace del router sin saber que los routers existen. El componente conserva sus reglas (foco, ARIA, teclado) y solo cede el elemento.
¿Cómo queda en un botón real?
Esta es una API permisiva, de las que crecen petición a petición:
type ButtonProps = {
primary?: boolean
secondary?: boolean
danger?: boolean
small?: boolean
href?: string
onClick?: () => void
icon?: ReactNode
style?: CSSProperties
}Ocho props opcionales, todas válidas junto a todas las demás. El mismo componente después de las seis reglas:
type ButtonBase = {
variant: 'primary' | 'secondary' | 'ghost'
tone?: 'neutral' | 'danger'
size?: 'small' | 'medium'
disabled?: boolean
children: ReactNode
}
type ButtonProps = ButtonBase & (
| { href: string; onClick?: never }
| { onClick: () => void; href?: never }
)
type IconButtonProps = Omit<ButtonBase, 'children'> & {
icon: IconName
label: string // becomes aria-label, and the tooltip
onClick: () => void
}Las variantes visuales son una sola prop. El peligro es un tono, así que se combina con cualquier variante que el diseño haya aprobado. Enlace y acción son dos ramas, y pasar las dos es un error de tipo. El botón de solo icono es un componente aparte, con la etiqueta obligatoria. La prop style ya no está: un aspecto que el sistema no contempla se convierte en la petición de una variante nueva, que es justo la conversación para la que existe el sistema.
¿Cuánto cuesta una API estricta?
Tres cosas, y conviene decirlas antes de que el equipo se comprometa.
- Los casos límite van más despacio. Un equipo de producto que necesita un aspecto no previsto espera una variante nueva o escribe su propio componente. La espera es intencionada, pero necesita un camino corto: un proceso de contribución que responde en pocos días mantiene a los equipos dentro del sistema. Cuando la respuesta tarda un trimestre, los equipos se van, y es una de las causas que recogemos en por qué se estanca la adopción de un design system.
- Los tipos solo protegen a quien usa TypeScript. Quien consume el componente en JavaScript puro no ve ningún error de compilación. A los tipos hay que sumarles avisos en tiempo de ejecución, solo en desarrollo, para las combinaciones más delicadas.
- Quitar una prop es un cambio incompatible. Endurecer una API existente supone una versión major y, a escala, un codemod. La migración a la v38 de Primer es un ejemplo público del tamaño de ese trabajo. Ser estrictos desde la primera release cuesta mucho menos que serlo después.
En nuestra propia web, una comprobación en el build rechaza los estilos inline, y cada clase puntual tiene que vivir en una única hoja de estilos con prefijo, con una nota que indica la tarea que la llevará al sistema. El número de excepciones se imprime en cada ejecución. Es la regla 5 aplicada a todo el código del sitio: la vía de escape existe, tiene nombre y alguien la cuenta. Una API más pequeña también es más fácil de mantener pequeña, que es lo que defendemos en la dieta del design system.
Preguntas frecuentes
Una sola className en el elemento raíz es un compromiso habitual: quien usa el componente puede colocarlo en el layout (márgenes, posición en la rejilla) sin tocar sus partes internas. Lo que no debería permitir es rediseñar el interior, porque esos overrides se rompen sin avisar cuando cambia el componente. Primer mantuvo className en la v38 y quitó sx, y React Spectrum guarda sus overrides bajo el prefijo UNSAFE_. Permitas lo que permitas, que se pueda encontrar con una búsqueda, para poder contarlo.
Las dos permiten que un componente se renderice como otro elemento. Una prop as cambia la etiqueta, así que los tipos del componente tienen que absorber las props de cada elemento en el que podría convertirse, y en TypeScript eso pesa enseguida. asChild, el patrón que usa Radix, pasa el renderizado al hijo, que conserva sus propias props tipadas. Cualquiera de los dos funciona si toda la librería usa el mismo; mezclarlos es lo que confunde a quien los usa.
En tres releases. Primero se añade la prop nueva junto a las antiguas y se muestra un aviso, solo en desarrollo, cada vez que se usan las antiguas. Después se pasa un codemod por los proyectos que usan el componente hasta que los avisos llegan a cero. Solo entonces se quitan las props antiguas, en una versión major, porque con el versionado semántico quitar una prop es un cambio incompatible. Saltarse la fase de avisos convierte la major en una sorpresa para cada equipo que actualiza.
Servicios relacionados
Artículos relacionados
Diseño
DiseñoPor qué quitamos componentes en vez de añadirlos: la dieta del design system
La mayoría de los design systems crecen hasta romperse. Tratamos el conteo de componentes como un número que mantener bajo. Las reglas que usamos para podarlo cada trimestre.
Diseño