Button
BetaLa acción principal de una interfaz. Cuatro estilos, cuatro tamaños, dos radios y cuatro modos de icono.
import { Button } from "@/registry/ds/button";
import { ArrowUpRight } from "@/registry/ds/icons";
export default function ButtonBasic() {
return (
<Button iconMode="pill" iconRight={<ArrowUpRight />}>
Inscribirme
</Button>
);
}Playground
Configurá el botón y copiá el snippet — el código se genera desde el mismo estado que renderiza el preview.
<Button>Inscribirme</Button>Jerarquía de estilos
Cada sección de una página tiene un solo Fill (la acción principal). El resto acompaña: Stroke para secundarias fuertes, Tonal para secundarias suaves, Text para terciarias y links.
import { Button } from "@/registry/ds/button";
export default function ButtonVariants() {
return (
<div className="flex flex-wrap items-center gap-4">
<Button variant="fill">Fill</Button>
<Button variant="stroke">Stroke</Button>
<Button variant="tonal">Tonal</Button>
<Button variant="text">Text</Button>
</div>
);
}Tamaños
El alto no es un número mágico: es padding del cuerpo + contenido, como en Figma. El label lleva su propio padding horizontal, y el icono ocupa un contenedor del alto interno.
| Size | Alto | Padding cuerpo | Label | Glifo | Tipografía |
|---|---|---|---|---|---|
sm | 32 | 4 | 24 / px 8 | 16 | text-sm |
md | 40 | 6 | 28 / px 10 | 20 | text-md |
lg | 48 | 8 | 32 / px 12 | 24 | text-md |
xl | 64 | 12 | 40 / px 16 | 24 | text-lg |
import { Button } from "@/registry/ds/button";
export default function ButtonSizes() {
return (
<div className="flex flex-wrap items-end gap-4">
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
<Button size="xl">Extra large</Button>
</div>
);
}Radius
El radio lo decide el contexto de la página, no el gusto: Full para superficies de marketing y vida estudiantil, Semi para lo institucional (radius token por tamaño: 6–12 px). Nunca mezcles radios dentro de una misma sección.
import { Button } from "@/registry/ds/button";
export default function ButtonRadius() {
return (
<div className="flex flex-wrap items-center gap-4">
<Button radius="full">Full — marketing</Button>
<Button radius="semi">Semi — institucional</Button>
</div>
);
}Iconos
Cuatro modos, como en el component set de Figma:
inside(default) — el glifo directo, en el color del label.pill— el glifo sobre un círculo de fondo (siempre redondo, incluso con radius semi). En Fill el círculo es claro y el glifo trackea el color de fondo del botón; en el resto el círculo es Primary/9 con glifo claro.outside— un círculo flotante del alto total del botón, separado 4 px del cuerpo, que replica el estilo del cuerpo (con semi se vuelve cuadrado redondeado). En Text no hay cuerpo: usa los círculos del pill.iconOnly— solo el glifo; en Text queda desnudo, sin caja.
iconLeft y iconRight aceptan cualquier icono y pueden combinarse.
import { Button } from "@/registry/ds/button";
import { ArrowUpRight, Plus, Search } from "@/registry/ds/icons";
export default function ButtonIcons() {
return (
<div className="flex flex-col gap-4">
{/* inside — el glifo directo, color del label */}
<div className="flex flex-wrap items-center gap-4">
<Button iconLeft={<Plus />}>Nueva materia</Button>
<Button variant="stroke" iconRight={<ArrowUpRight />}>
Ver programa
</Button>
</div>
{/* pill — círculo de fondo alrededor del glifo */}
<div className="flex flex-wrap items-center gap-4">
<Button iconMode="pill" iconRight={<ArrowUpRight />}>
Inscribirme
</Button>
<Button variant="tonal" iconMode="pill" iconLeft={<ArrowUpRight />}>
Conocer el campus
</Button>
</div>
{/* outside — círculo flotante fuera del cuerpo */}
<div className="flex flex-wrap items-center gap-4">
<Button iconMode="outside" iconRight={<ArrowUpRight />}>
Empezar ahora
</Button>
<Button variant="stroke" iconMode="outside" iconLeft={<ArrowUpRight />}>
Ver carreras
</Button>
</div>
{/* icon only — requiere aria-label */}
<div className="flex flex-wrap items-center gap-4">
<Button iconOnly={<Search />} aria-label="Buscar" />
<Button variant="stroke" iconOnly={<Plus />} aria-label="Agregar" />
<Button variant="text" iconOnly={<ArrowUpRight />} aria-label="Abrir" />
</div>
</div>
);
}Cuándo usarlo
- Sí — para acciones: enviar, inscribirse, confirmar, abrir un flujo.
- Sí —
pillyoutsidepara CTAs de marketing donde el botón es protagonista;insidepara UI de producto. - Sí — icon-only para acciones de utilidad reconocibles (buscar, cerrar),
siempre con
aria-label. - No — para navegar a otra página dentro del contenido: eso es un link
(
variant="text"existe justamente para esa zona gris). - No — más de un Fill por sección; si todo es principal, nada lo es.
Accesibilidad
- El focus ring (
Primary/8, 2px) viene incluido — en modooutsideenvuelve cada pieza (cuerpo y círculos), como en Figma. No lo quites ni lo tapes. - Todo el conjunto (círculos flotantes incluidos) es un solo botón: un target de click, un stop de tab.
disabledbaja la opacidad a 0.38 y bloquea el pointer; si la acción puede volver a habilitarse, explicá cómo cerca del botón.- En icon-only el
aria-labeles obligatorio.
Props
| Prop | Tipo | Default | Descripción |
|---|---|---|---|
variant | "fill" | "stroke" | "tonal" | "text" | "fill" | Jerarquía visual del botón. |
size | "sm" | "md" | "lg" | "xl" | "md" | Altura 32 / 40 / 48 / 64 px, con tipografía y padding acordes. |
radius | "full" | "semi" | "full" | Full = pill; Semi usa el radius token del tamaño (6–12 px). |
iconMode | "inside" | "pill" | "outside" | "inside" | Cómo se presentan iconLeft/iconRight: directo, con círculo de fondo, o flotante fuera del cuerpo. |
iconLeft | ReactNode | — | Icono antes del label. |
iconRight | ReactNode | — | Icono después del label. |
iconOnly | ReactNode | — | Solo icono; requiere aria-label. Oculta el label e ignora iconMode. |
disabled | boolean | false | Opacidad 0.38 + pointer-events bloqueados. |