Saltar al contenido
VEXA
Acciones

Button

Apto para servidor

Triggers an action or submits a form.

Vista previa en vivo

Renderizado desde el código fuente instalado de VEXA — los mismos archivos que recibes.

Escritorio

Playground

Ajusta las props y copia el JSX equivalente.

Playground

button.tsx
<Button>Button</Button>

Instalación

Añade el código del componente a tu proyecto. Las dependencias del registry se resuelven automáticamente.

npx shadcn@latest add https://vexa.valfiguer.com/r/button.json

Se instala en components/vexa/button.tsx

Dependencias npm

  • @radix-ui/react-slot
  • class-variance-authority

Dependencias del registry

  • @vexa/utils

Otros items de VEXA que necesita este componente. Instálalos primero — la CLI los resuelve automáticamente.

Código

Cópialo a su ruta destino, o instálalo con el comando de arriba.

components/vexa/button.tsx
import type * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";

import { cn } from "@/lib/utils";

export const buttonVariants = cva(
  [
    "inline-flex select-none items-center justify-center gap-2 whitespace-nowrap",
    "rounded-vexa-md text-sm font-medium",
    "transition-[color,background-color,border-color,box-shadow]",
    "duration-[var(--vexa-duration-fast)] ease-[var(--vexa-ease-standard)]",
    "outline-none focus-visible:ring-2 focus-visible:ring-vexa-ring",
    "focus-visible:ring-offset-2 focus-visible:ring-offset-vexa-background",
    "disabled:pointer-events-none disabled:opacity-50",
    "aria-[busy=true]:pointer-events-none aria-[busy=true]:cursor-progress",
    "[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
  ],
  {
    variants: {
      variant: {
        primary:
          "bg-vexa-primary text-vexa-primary-foreground hover:bg-vexa-primary/90 active:bg-vexa-primary/95",
        secondary:
          "border border-vexa-border bg-vexa-secondary text-vexa-secondary-foreground hover:bg-vexa-accent active:bg-vexa-accent",
        outline:
          "border border-vexa-input bg-transparent text-vexa-foreground hover:bg-vexa-accent hover:text-vexa-accent-foreground active:bg-vexa-accent",
        ghost:
          "bg-transparent text-vexa-foreground hover:bg-vexa-accent hover:text-vexa-accent-foreground active:bg-vexa-accent",
        danger:
          "bg-vexa-danger text-vexa-danger-foreground hover:bg-vexa-danger/90 active:bg-vexa-danger/95",
        link: "bg-transparent text-vexa-focus underline-offset-4 hover:underline focus-visible:ring-offset-0",
      },
      size: {
        sm: "h-8 px-3 text-xs",
        md: "h-10 px-4",
        lg: "h-12 px-6 text-base",
      },
    },
    defaultVariants: {
      variant: "primary",
      size: "md",
    },
  },
);

export interface ButtonProps
  extends React.ComponentProps<"button">, VariantProps<typeof buttonVariants> {
  /**
   * Render the button's styles onto its single child element instead of a
   * native `<button>` (e.g. to style a link). The child owns its own
   * semantics; ensure it remains keyboard-operable.
   */
  asChild?: boolean;
}

/**
 * Button — triggers an action or submits a form.
 *
 * RSC-safe: contains no state or effects, so it renders on the server. Attach
 * `onClick` from a Client Component when interactivity is required. Refs are
 * accepted as a regular prop (React 19).
 */
export function Button({
  className,
  variant,
  size,
  asChild = false,
  type,
  ...props
}: ButtonProps) {
  const Comp = asChild ? Slot : "button";
  return (
    <Comp
      data-slot="button"
      className={cn(buttonVariants({ variant, size }), className)}
      type={asChild ? undefined : (type ?? "button")}
      {...props}
    />
  );
}
JSON del registryr/button.json
r/button.json
{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "button",
  "type": "registry:ui",
  "title": "Button",
  "description": "Triggers an action or submits a form.",
  "dependencies": [
    "@radix-ui/react-slot",
    "class-variance-authority"
  ],
  "registryDependencies": [
    "@vexa/utils"
  ],
  "files": [
    {
      "path": "registry/default/ui/button.tsx",
      "type": "registry:ui",
      "target": "components/vexa/button.tsx",
      "content": "import type * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport const buttonVariants = cva(\n  [\n    \"inline-flex select-none items-center justify-center gap-2 whitespace-nowrap\",\n    \"rounded-vexa-md text-sm font-medium\",\n    \"transition-[color,background-color,border-color,box-shadow]\",\n    \"duration-[var(--vexa-duration-fast)] ease-[var(--vexa-ease-standard)]\",\n    \"outline-none focus-visible:ring-2 focus-visible:ring-vexa-ring\",\n    \"focus-visible:ring-offset-2 focus-visible:ring-offset-vexa-background\",\n    \"disabled:pointer-events-none disabled:opacity-50\",\n    \"aria-[busy=true]:pointer-events-none aria-[busy=true]:cursor-progress\",\n    \"[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n  ],\n  {\n    variants: {\n      variant: {\n        primary:\n          \"bg-vexa-primary text-vexa-primary-foreground hover:bg-vexa-primary/90 active:bg-vexa-primary/95\",\n        secondary:\n          \"border border-vexa-border bg-vexa-secondary text-vexa-secondary-foreground hover:bg-vexa-accent active:bg-vexa-accent\",\n        outline:\n          \"border border-vexa-input bg-transparent text-vexa-foreground hover:bg-vexa-accent hover:text-vexa-accent-foreground active:bg-vexa-accent\",\n        ghost:\n          \"bg-transparent text-vexa-foreground hover:bg-vexa-accent hover:text-vexa-accent-foreground active:bg-vexa-accent\",\n        danger:\n          \"bg-vexa-danger text-vexa-danger-foreground hover:bg-vexa-danger/90 active:bg-vexa-danger/95\",\n        link: \"bg-transparent text-vexa-focus underline-offset-4 hover:underline focus-visible:ring-offset-0\",\n      },\n      size: {\n        sm: \"h-8 px-3 text-xs\",\n        md: \"h-10 px-4\",\n        lg: \"h-12 px-6 text-base\",\n      },\n    },\n    defaultVariants: {\n      variant: \"primary\",\n      size: \"md\",\n    },\n  },\n);\n\nexport interface ButtonProps\n  extends React.ComponentProps<\"button\">, VariantProps<typeof buttonVariants> {\n  /**\n   * Render the button's styles onto its single child element instead of a\n   * native `<button>` (e.g. to style a link). The child owns its own\n   * semantics; ensure it remains keyboard-operable.\n   */\n  asChild?: boolean;\n}\n\n/**\n * Button — triggers an action or submits a form.\n *\n * RSC-safe: contains no state or effects, so it renders on the server. Attach\n * `onClick` from a Client Component when interactivity is required. Refs are\n * accepted as a regular prop (React 19).\n */\nexport function Button({\n  className,\n  variant,\n  size,\n  asChild = false,\n  type,\n  ...props\n}: ButtonProps) {\n  const Comp = asChild ? Slot : \"button\";\n  return (\n    <Comp\n      data-slot=\"button\"\n      className={cn(buttonVariants({ variant, size }), className)}\n      type={asChild ? undefined : (type ?? \"button\")}\n      {...props}\n    />\n  );\n}\n"
    }
  ],
  "meta": {
    "frameworks": [
      "react",
      "next"
    ],
    "rsc": "server-safe",
    "variants": {
      "variant": [
        "primary",
        "secondary",
        "outline",
        "ghost",
        "danger",
        "link"
      ],
      "size": [
        "sm",
        "md",
        "lg"
      ]
    },
    "cssVars": [
      "--vexa-primary",
      "--vexa-primary-foreground",
      "--vexa-secondary",
      "--vexa-secondary-foreground",
      "--vexa-accent",
      "--vexa-accent-foreground",
      "--vexa-danger",
      "--vexa-danger-foreground",
      "--vexa-border",
      "--vexa-input",
      "--vexa-focus",
      "--vexa-ring",
      "--vexa-background"
    ],
    "a11y": "Visible focus-visible ring; native disabled; aria-busy shows a progress cursor. `asChild` children must remain keyboard-operable.",
    "example": "<Button variant=\"primary\" size=\"md\">Save changes</Button>",
    "tests": "registry/tests/button.test.tsx"
  }
}

Detalles

Variantes

variant
primarysecondaryoutlineghostdangerlink
size
smmdlg

Accesibilidad

Visible focus-visible ring; native disabled; aria-busy shows a progress cursor. `asChild` children must remain keyboard-operable.

Uso

Trigger an action or submit a form. Use `variant` for emphasis (one `primary` per view) and `size` for density; render as another element with `asChild` (e.g. a link that looks like a button).

Anatomía

Button
The element; renders a native `<button>`, or its child when `asChild` is set.
buttonVariants
cva helper to reuse the button's variant/size classes on another element.

Accesibilidad

Native `<button>` semantics. With `asChild` the semantics come from the child, which must stay focusable and operable.

Teclado

TeclaAcción
Enter / SpaceActivate the button

Foco: Visible `focus-visible` ring. `disabled` uses the native attribute (removed from the tab order); a loading button sets `aria-busy` and shows a progress cursor.

  • Variant meaning (e.g. `danger`) is carried by the label text, not color alone (WCAG 1.4.1).

Buenas prácticas

Recomendado

  • Give every button a text label.
  • Use exactly one `primary` action per view.
  • Use `asChild` so a link keeps real `<a>` semantics.

Evitar

  • Don't use a Button for navigation — use a link.
  • Don't remove the focus ring.
  • Don't make a disabled submit button the only validation feedback.
Editar esta página

¿Te ha resultado útil esta página?

Guardado localmente en este navegador. Sin cuenta ni servidor.