Button
Acción en forma de píldora con cuatro tonos: primary, secondary, accent y ghost.
Instalación
$ pnpm dlx shadcn@latest add @ssntl/buttonUso
import { Button } from "@/components/ui/button"
<Button>Agendar llamada</Button>
<Button variant="secondary">Enviar correo</Button>
<Button variant="accent">Publicar</Button>
<Button asChild variant="ghost">
<a href="/precios">Ver precios</a>
</Button>- primary es la llamada a la acción por defecto: una por vista.
- accent es solo para la acción que confirma (Publicar, Enviar, Pagar). Nunca dos en una pantalla.
- ghost para Cancelar, Omitir y acciones terciarias.
- Tamaños: md 48px (igual que inputs), sm 36px para filas densas, lg 56px para heroes.
- Verbo primero, en oración, dos o tres palabras. Sin mayúsculas sostenidas ni iconos a ambos lados.
- Los botones solo de icono necesitan aria-label. Marca el ícono de un botón con texto con data-icon para el padding óptico.
Código
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
// Essential Design `es-btn`: a pill, 48px by default, that scales to 0.96 on
// press. Hover only exists on pointer devices (Tailwind v4 `hover:`).
const buttonVariants = cva(
[
"inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-full border border-transparent font-sans font-medium whitespace-nowrap select-none",
"transition-[scale,background-color,color] duration-(--duration-fast) ease-out active:scale-(--es-press) motion-reduce:active:scale-100",
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
"disabled:pointer-events-none disabled:opacity-40 aria-disabled:pointer-events-none aria-disabled:opacity-40",
"[&_svg]:pointer-events-none [&_svg]:shrink-0",
],
{
variants: {
variant: {
primary: "bg-primary text-primary-foreground hover:bg-primary/85",
secondary:
"bg-secondary text-secondary-foreground hover:bg-neutral-200 dark:hover:bg-neutral-600",
// The one commit action of a tool (Publish, Send, Pay). Never two per screen.
accent: "bg-blue-600 text-white shadow-brand hover:bg-blue-600/90",
ghost:
"bg-transparent px-4 text-muted-foreground hover:bg-secondary hover:text-foreground",
},
size: {
// Optical alignment: the side with an icon (data-icon) takes 4px less
// padding, so text and icon read as centered.
sm: "h-9 px-4 text-caption font-medium has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3 [&_svg]:size-4",
md: "h-12 px-6 text-label has-data-[icon=inline-end]:pr-5 has-data-[icon=inline-start]:pl-5 [&_svg]:size-5",
lg: "h-14 px-8 text-body font-medium has-data-[icon=inline-end]:pr-7 has-data-[icon=inline-start]:pl-7 [&_svg]:size-5",
"icon-sm": "size-9 [&_svg]:size-4",
icon: "size-12 [&_svg]:size-5",
"icon-lg": "size-14 [&_svg]:size-5",
},
},
defaultVariants: {
variant: "primary",
size: "md",
},
}
)
type ButtonProps = React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
/** Render the child element (a link, for example) with the button's styles. */
asChild?: boolean
}
function Button({
className,
variant,
size,
asChild = false,
type,
...props
}: ButtonProps) {
const Comp = asChild ? Slot.Root : "button"
return (
<Comp
data-slot="button"
data-variant={variant ?? "primary"}
type={asChild ? type : (type ?? "button")}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants, type ButtonProps }