Skip to content
VEXA
Actions

Button

Server-safe

Triggers an action or submits a form.

Live preview

Rendered from the installed VEXA source — the same files you receive.

Desktop

Playground

Adjust the props and copy the matching JSX.

Playground

button.tsx
<Button>Button</Button>

Installation

Adds the component source to your project. Registry dependencies are pulled in automatically.

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

Installs to components/vexa/button.tsx

npm dependencies

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

Registry dependencies

  • @vexa/utils

Other VEXA items this component needs. Install them first — they are resolved automatically by the CLI.

Source

Copy this into its target path, or install it with the command above.

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}
    />
  );
}
Registry JSONr/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"
  }
}

Details

Variants

variant
primarysecondaryoutlineghostdangerlink
size
smmdlg

Accessibility

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