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.json

Live 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

PropTypeDefaultDescription
variant"solid" | "outline" | "soft" | "ghost" | "link" | "destructive"solidVisual style.
size"xs" | "sm" | "md" | "lg" | "icon" | "icon-sm" | "icon-lg"mdHeight, padding and icon scale.
fullWidthbooleanStretches the button to fill its container.
asChildbooleanfalseRender the child element instead of a <button>, via Slot.
loadingbooleanfalseShows a spinner, disables interaction, hides leadingIcon, sets aria-busy.
loadingLabelstringLoadingScreen-reader-only text announced while loading.
leadingIconReact.ReactNodeElement rendered before the label.
trailingIconReact.ReactNodeElement rendered after the label.
type"button" | "submit" | "reset"buttonNative button type. Defaults to "button" so buttons inside forms do not submit accidentally.
disabledbooleanNative disabled attribute.
classNamestringMerged 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

Browse the full examples gallery