Button
Server-safeTriggers 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.jsonInstalls 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.