Saltar al contenido
ssntl/ui

Button

Acción en forma de píldora con cuatro tonos: primary, secondary, accent y ghost.

Instalación

$ pnpm dlx shadcn@latest add @ssntl/button

Uso

tsx
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

components/ui/button.tsx
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 }