Base
Button
Button wraps a native <button> element (or any element via asChild) with Tailwind variants, an extended size scale, loading state, and leading/trailing icon slots. Use `buttonVariants` with a plain <a> for navigation links that must keep link semantics.
Installation
npx shadcn add https://rck.vibeboxph.com//r/button.jsonLive preview & controls
Controls
<Button>Button</Button>Anatomy
ButtonThe root <button> (or Slot target when asChild).leadingIcon / trailingIconOptional icon slots either side of the label.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | "solid" | "outline" | "soft" | "ghost" | "link" | "destructive" | solid | Visual style. |
| size | "xs" | "sm" | "md" | "lg" | "icon" | "icon-sm" | "icon-lg" | md | Height, padding and icon scale. |
| fullWidth | boolean | — | Stretches the button to fill its container. |
| asChild | boolean | false | Render the child element instead of a <button>, via Slot. |
| loading | boolean | false | Shows a spinner, disables interaction, hides leadingIcon, sets aria-busy. |
| loadingLabel | string | Loading | Screen-reader-only text announced while loading. |
| leadingIcon | React.ReactNode | — | Element rendered before the label. |
| trailingIcon | React.ReactNode | — | Element rendered after the label. |
| type | "button" | "submit" | "reset" | button | Native button type. Defaults to "button" so buttons inside forms do not submit accidentally. |
| disabled | boolean | — | Native disabled attribute. |
| className | string | — | Merged with the generated variant classes via cn(). |
Accessibility
- Renders a native <button> by default, so it's keyboard-focusable and activates on Enter/Space without extra ARIA.
- Defaults to `type="button"` inside forms so incidental clicks do not submit the form.
- `loading` sets `aria-busy`, disables the button, and renders a screen-reader-only `loadingLabel` so progress is announced.
- Icon-only buttons must include an accessible name via `aria-label` or visible text inside the button.
- For navigation, prefer `buttonVariants` on a plain `<a>` (or framework link) instead of `<Button asChild>` so link semantics and browser affordances (open in new tab, URL preview) are preserved.
- Focus is always visible via `focus-visible:ring-2`, never suppressed.
- When `asChild` is used with a non-button element, pass `aria-disabled` via the child if you need disabled styling without the native `disabled` attribute.
Source
registry/rck/ui/button.tsx
import * as React from "react";
import { cva, type VariantProps } from "@/lib/cva";
import { LoaderCircle } from "@/lib/icons";
import { Slot } from "@/registry/rck/primitives/slot";
import { cn } from "@/lib/utils";
export const buttonVariants = cva(
"inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-[11px] font-semibold " +
"transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring " +
"focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none " +
"disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 " +
"[&_[data-icon=inline-start]]:-ml-0.5 [&_[data-icon=inline-end]]:-mr-0.5",
{
variants: {
variant: {
solid: "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90",
outline:
"border border-border-strong bg-transparent text-foreground shadow-sm hover:bg-surface-2",
soft: "bg-primary-soft text-primary-soft-foreground hover:bg-primary-soft/80",
ghost: "bg-transparent text-foreground hover:bg-surface-2",
link: "bg-transparent text-primary underline-offset-4 hover:underline",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
},
size: {
xs: "h-7 px-2 text-[10px] [&_svg]:size-3",
sm: "h-8 px-2.5 text-[10px] [&_svg]:size-3",
md: "h-[39px] px-[15px] [&_svg]:size-3.5",
lg: "h-11 px-5 text-xs [&_svg]:size-4",
icon: "size-[39px] p-0 [&_svg]:size-4",
"icon-sm": "size-8 p-0 [&_svg]:size-3.5",
"icon-lg": "size-11 p-0 [&_svg]:size-4",
},
fullWidth: {
true: "w-full",
},
},
defaultVariants: {
variant: "solid",
size: "md",
},
},
);
export interface ButtonProps
extends
React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
/** Render the child element instead of a `<button>`, forwarding all props (Radix Slot). */
asChild?: boolean;
/** Show a spinner and disable interaction, keeping the button's width. */
loading?: boolean;
/** Screen-reader-only label announced while `loading` is true. */
loadingLabel?: string;
/** Element rendered before the label. */
leadingIcon?: React.ReactNode;
/** Element rendered after the label. */
trailingIcon?: React.ReactNode;
}
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
(
{
className,
variant,
size,
fullWidth,
asChild = false,
loading = false,
loadingLabel = "Loading",
disabled,
leadingIcon,
trailingIcon,
type,
children,
...props
},
ref,
) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
ref={ref}
data-slot="button"
type={asChild ? undefined : (type ?? "button")}
className={cn(buttonVariants({ variant, size, fullWidth }), className)}
disabled={disabled || loading}
aria-busy={loading || undefined}
aria-disabled={asChild && (disabled || loading) ? true : undefined}
{...props}
>
{asChild ? (
children
) : (
<>
{loading ? (
<LoaderCircle
className="animate-spin"
aria-hidden="true"
data-icon="inline-start"
/>
) : leadingIcon ? (
<span data-icon="inline-start">{leadingIcon}</span>
) : null}
{children}
{loading ? (
<span className="sr-only">{loadingLabel}</span>
) : trailingIcon ? (
<span data-icon="inline-end">{trailingIcon}</span>
) : null}
</>
)}
</Comp>
);
},
);
Button.displayName = "Button";
Related examples
VariantsSolid, outline, soft, ghost, link and destructive.
SizesExtra-small through large on the default scale.
IconsLeading, trailing and icon-only buttons.
LoadingStatic and interactive loading states.
DisabledDisabled state across variants.
As linkStyled anchors via `buttonVariants` for navigation.
LayoutFull width, pill shape, and icon size scale.