Button
A versatile button component with multiple variants and sizes. Built with CVA and CSS Modules — copy it into your project and own it completely.
npx visor add buttonVariants
Sizes
Disabled State
Gated State
A permission-gated button is visually dimmed but remains hover-able, surfacing a tooltip that explains why the action is unavailable. Unlike the native disabled state, the button stays in the tab order so assistive technology users can discover the reason.
Use gated to apply the visual state, and gatedReason to provide the tooltip text. A TooltipProvider ancestor is required when gatedReason is set.
The gated prop uses aria-disabled="true" instead of the native disabled attribute, so the button stays keyboard-focusable. Click handlers are suppressed internally — no need to guard them in your code.
Installation
npx visor add buttonThis copies two files into your project:
components/ui/button/button.tsx— the componentcomponents/ui/button/button.module.css— the styles
Usage
import { Button } from '@/components/ui/button/button';
export default function Example() {
return <Button>Click me</Button>;
}API Reference
ButtonProps
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'default' | 'secondary' | 'outline' | 'ghost' | 'destructive' | 'default' | Visual style variant of the button. |
size | 'sm' | 'md' | 'lg' | 'md' | Size of the button, affecting height and padding. |
asChild | boolean | false | Merge props onto the immediate child element instead of rendering a <button>. |
disabled | boolean | false | Disables the button and applies reduced-opacity styling. |
className | string | — | Additional CSS class names to merge onto the element. |
gated | boolean | false | Renders the button in a permission-gated state: visually dimmed (opacity 0.4), cursor not-allowed, and click handlers suppressed. Uses aria-disabled instead of native disabled so the button remains keyboard-focusable (required for the tooltip to be accessible). |
gatedReason | string | — | Explanation surfaced in an anchored tooltip when the button is gated. Only rendered when gated is also true. Requires a TooltipProvider ancestor. Example: "You can't delete this — you're not an owner". |
onClick | React.MouseEventHandler<HTMLButtonElement> | — | Click event handler. |
...props | React.ButtonHTMLAttributes<HTMLButtonElement> | — | All standard HTML button attributes are forwarded. |
The component also accepts all standard <button> HTML attributes.
Source Files
After running npx visor add button, you'll have:
button.tsx
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import styles from "./button.module.css";
const buttonVariants = cva(styles.base, {
variants: {
variant: {
default: styles.variantDefault,
secondary: styles.variantSecondary,
outline: styles.variantOutline,
ghost: styles.variantGhost,
destructive: styles.variantDestructive,
},
size: {
sm: styles.sizeSm,
md: styles.sizeMd,
lg: styles.sizeLg,
},
},
defaultVariants: {
variant: "default",
size: "md",
},
});
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, ...props }, ref) => {
return (
<button
className={cn(buttonVariants({ variant, size }), className)}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = "Button";
export { Button, buttonVariants };button.module.css
All styles use CSS custom properties from @loworbitstudio/visor-core, so the button automatically adapts to your active theme.
Customization
After copying the component, you own it completely. Common customizations:
// Add a loading state
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, loading, children, ...props }, ref) => {
return (
<button
className={cn(buttonVariants({ variant, size }), className)}
disabled={loading || props.disabled}
ref={ref}
{...props}
>
{loading ? <Spinner size={16} /> : children}
</button>
);
}
);