Button
A button with five variants, four sizes and a loading state that keeps focus and announces progress, on Radix, Base UI or React Aria.
Not yet available: this combination has no tested variant yet. Pick another option above.
Install
Item button-radix-lucide · registry JSON
npx shadcn@latest add https://labs.designfrom.com/r/button-radix-lucide.jsonnpx shadcn@latest add @designfrom/button-radix-lucideOnce, in components.json: "registries": { "@designfrom": "https://labs.designfrom.com/r/{name}.json" }
npx designfrom add buttonnpm i @designfrom/reactimport { Button, buttonVariants } from "@designfrom/react/radix/button";Source
Comments are kept. The header lists provenance, license and install commands for people and coding assistants.
import { cva, type VariantProps } from "class-variance-authority";
import { LoaderCircle } from "lucide-react";
import { Slot } from "radix-ui";
import type * as React from "react";
import { cn } from "@/lib/utils";
/**
* @dfl-header: v1, generated by labs.designfrom.com at build time
* Item: button-radix-lucide (Button: Radix, Lucide)
* What: A button with five variants, four sizes and a loading state that
* keeps focus and announces progress, on Radix, Base UI or React Aria.
* Status: ready. License: MIT. Author: Design From Labs
* <https://labs.designfrom.com>
* Source: https://github.com/Design-from-labs/labs @ 803b92c1a30e (examples/button)
* Credits: S-003 React (MIT); S-004 shadcn/ui and its registry schema (MIT);
* S-005 Lucide icons (ISC); S-006 Phosphor Icons (MIT); S-008
* Tailwind CSS (MIT); S-018 class-variance-authority (Apache-2.0);
* S-032 Radix Primitives (the unified radix-ui package) (MIT);
* S-033 Base UI (@base-ui/react) (MIT); S-034 React Aria Components
* (react-aria-components) (Apache-2.0)
* Install: npx shadcn@latest add https://labs.designfrom.com/r/button-radix-lucide.json
* npx shadcn@latest add @designfrom/button-radix-lucide
* npx designfrom add button
* npm i @designfrom/react, then import { Button, buttonVariants } from "@designfrom/react/radix/button";
* Requires: class-variance-authority, radix-ui, lucide-react, registry: utils
* Docs: https://labs.designfrom.com/ui/button (index: https://labs.designfrom.com/llms.txt)
*/
// Every color comes from the shared token layer (primary, accent, ring...),
// so the button follows whatever theme the host project defines.
const buttonVariants = cva(
"inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm outline-none transition-[color,background-color,box-shadow] duration-150 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 aria-busy:cursor-progress motion-reduce:transition-none [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
ghost: "hover:bg-accent hover:text-accent-foreground",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
},
size: {
sm: "h-8 px-3 text-xs",
md: "h-9 px-4",
lg: "h-10 px-6",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "md",
},
},
);
export interface ButtonProps
extends React.ComponentProps<"button">,
VariantProps<typeof buttonVariants> {
/** Render the child element instead of a button (Radix Slot), e.g. a link. */
asChild?: boolean;
/** Shows a spinner, sets aria-busy and ignores clicks while true. */
loading?: boolean;
}
function Button({
className,
variant,
size,
asChild = false,
loading = false,
type,
onClick,
children,
...props
}: ButtonProps) {
const Comp = asChild ? Slot.Root : "button";
return (
<Comp
data-slot="button"
// A Slot child keeps its own semantics; only a real button gets a type.
type={asChild ? undefined : (type ?? "button")}
{...props}
// aria-disabled (not disabled) keeps keyboard focus on the button
// while the action runs; the click guard below does the blocking.
aria-busy={loading || undefined}
aria-disabled={loading || props["aria-disabled"] || undefined}
className={cn(buttonVariants({ variant, size }), className)}
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
if (loading) {
event.preventDefault();
return;
}
onClick?.(event);
}}
>
{asChild ? (
children
) : (
<>
{loading ? (
<LoaderCircle
aria-hidden="true"
className="motion-safe:animate-spin"
/>
) : null}
{children}
</>
)}
</Comp>
);
}
export { Button, buttonVariants };Dependencies
Install
Item button-radix-phosphor · registry JSON
npx shadcn@latest add https://labs.designfrom.com/r/button-radix-phosphor.jsonnpx shadcn@latest add @designfrom/button-radix-phosphorOnce, in components.json: "registries": { "@designfrom": "https://labs.designfrom.com/r/{name}.json" }
npx designfrom add button --icons phosphornpm i @designfrom/reactimport { Button, buttonVariants } from "@designfrom/react/radix/button/phosphor";Source
Comments are kept. The header lists provenance, license and install commands for people and coding assistants.
import { cva, type VariantProps } from "class-variance-authority";
import { CircleNotchIcon } from "@phosphor-icons/react/ssr";
import { Slot } from "radix-ui";
import type * as React from "react";
import { cn } from "@/lib/utils";
/**
* @dfl-header: v1, generated by labs.designfrom.com at build time
* Item: button-radix-phosphor (Button: Radix, Phosphor)
* What: A button with five variants, four sizes and a loading state that
* keeps focus and announces progress, on Radix, Base UI or React Aria.
* Status: ready. License: MIT. Author: Design From Labs
* <https://labs.designfrom.com>
* Source: https://github.com/Design-from-labs/labs @ 803b92c1a30e (examples/button)
* Credits: S-003 React (MIT); S-004 shadcn/ui and its registry schema (MIT);
* S-005 Lucide icons (ISC); S-006 Phosphor Icons (MIT); S-008
* Tailwind CSS (MIT); S-018 class-variance-authority (Apache-2.0);
* S-032 Radix Primitives (the unified radix-ui package) (MIT);
* S-033 Base UI (@base-ui/react) (MIT); S-034 React Aria Components
* (react-aria-components) (Apache-2.0)
* Install: npx shadcn@latest add https://labs.designfrom.com/r/button-radix-phosphor.json
* npx shadcn@latest add @designfrom/button-radix-phosphor
* npx designfrom add button --icons phosphor
* npm i @designfrom/react, then import { Button, buttonVariants } from "@designfrom/react/radix/button/phosphor";
* Requires: class-variance-authority, radix-ui, @phosphor-icons/react, registry: utils
* Docs: https://labs.designfrom.com/ui/button (index: https://labs.designfrom.com/llms.txt)
*/
// Every color comes from the shared token layer (primary, accent, ring...),
// so the button follows whatever theme the host project defines.
const buttonVariants = cva(
"inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm outline-none transition-[color,background-color,box-shadow] duration-150 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 aria-busy:cursor-progress motion-reduce:transition-none [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
ghost: "hover:bg-accent hover:text-accent-foreground",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
},
size: {
sm: "h-8 px-3 text-xs",
md: "h-9 px-4",
lg: "h-10 px-6",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "md",
},
},
);
export interface ButtonProps
extends React.ComponentProps<"button">,
VariantProps<typeof buttonVariants> {
/** Render the child element instead of a button (Radix Slot), e.g. a link. */
asChild?: boolean;
/** Shows a spinner, sets aria-busy and ignores clicks while true. */
loading?: boolean;
}
function Button({
className,
variant,
size,
asChild = false,
loading = false,
type,
onClick,
children,
...props
}: ButtonProps) {
const Comp = asChild ? Slot.Root : "button";
return (
<Comp
data-slot="button"
// A Slot child keeps its own semantics; only a real button gets a type.
type={asChild ? undefined : (type ?? "button")}
{...props}
// aria-disabled (not disabled) keeps keyboard focus on the button
// while the action runs; the click guard below does the blocking.
aria-busy={loading || undefined}
aria-disabled={loading || props["aria-disabled"] || undefined}
className={cn(buttonVariants({ variant, size }), className)}
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
if (loading) {
event.preventDefault();
return;
}
onClick?.(event);
}}
>
{asChild ? (
children
) : (
<>
{loading ? (
<CircleNotchIcon
aria-hidden="true"
className="motion-safe:animate-spin"
/>
) : null}
{children}
</>
)}
</Comp>
);
}
export { Button, buttonVariants };Dependencies
Install
Item button-base-lucide · registry JSON
npx shadcn@latest add https://labs.designfrom.com/r/button-base-lucide.jsonnpx shadcn@latest add @designfrom/button-base-lucideOnce, in components.json: "registries": { "@designfrom": "https://labs.designfrom.com/r/{name}.json" }
npx designfrom add button --primitive basenpm i @designfrom/reactimport { Button, buttonVariants } from "@designfrom/react/base/button";Source
Comments are kept. The header lists provenance, license and install commands for people and coding assistants.
import { Button as ButtonPrimitive } from "@base-ui/react/button";
import { cva, type VariantProps } from "class-variance-authority";
import { LoaderCircle } from "lucide-react";
import { cn } from "@/lib/utils";
/**
* @dfl-header: v1, generated by labs.designfrom.com at build time
* Item: button-base-lucide (Button: Base UI, Lucide)
* What: A button with five variants, four sizes and a loading state that
* keeps focus and announces progress, on Radix, Base UI or React Aria.
* Status: ready. License: MIT. Author: Design From Labs
* <https://labs.designfrom.com>
* Source: https://github.com/Design-from-labs/labs @ 803b92c1a30e (examples/button)
* Credits: S-003 React (MIT); S-004 shadcn/ui and its registry schema (MIT);
* S-005 Lucide icons (ISC); S-006 Phosphor Icons (MIT); S-008
* Tailwind CSS (MIT); S-018 class-variance-authority (Apache-2.0);
* S-032 Radix Primitives (the unified radix-ui package) (MIT);
* S-033 Base UI (@base-ui/react) (MIT); S-034 React Aria Components
* (react-aria-components) (Apache-2.0)
* Install: npx shadcn@latest add https://labs.designfrom.com/r/button-base-lucide.json
* npx shadcn@latest add @designfrom/button-base-lucide
* npx designfrom add button --primitive base
* npm i @designfrom/react, then import { Button, buttonVariants } from "@designfrom/react/base/button";
* Requires: class-variance-authority, @base-ui/react, lucide-react, registry: utils
* Docs: https://labs.designfrom.com/ui/button (index: https://labs.designfrom.com/llms.txt)
*/
// Every color comes from the shared token layer (primary, accent, ring...),
// so the button follows whatever theme the host project defines.
const buttonVariants = cva(
"inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm outline-none transition-[color,background-color,box-shadow] duration-150 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 aria-busy:cursor-progress motion-reduce:transition-none [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
ghost: "hover:bg-accent hover:text-accent-foreground",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
},
size: {
sm: "h-8 px-3 text-xs",
md: "h-9 px-4",
lg: "h-10 px-6",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "md",
},
},
);
export interface ButtonProps
extends Omit<ButtonPrimitive.Props, "className">,
VariantProps<typeof buttonVariants> {
className?: string;
/** Shows a spinner, sets aria-busy and disables the button while true. */
loading?: boolean;
}
function Button({
className,
variant,
size,
loading = false,
disabled,
children,
...props
}: ButtonProps) {
return (
<ButtonPrimitive
data-slot="button"
// Base UI keeps a disabled button focusable on request, so focus is
// not lost while the action runs.
disabled={disabled || loading}
focusableWhenDisabled={loading}
aria-busy={loading || undefined}
className={cn(buttonVariants({ variant, size }), className)}
{...props}
>
{loading ? (
<LoaderCircle aria-hidden="true" className="motion-safe:animate-spin" />
) : null}
{children}
</ButtonPrimitive>
);
}
export { Button, buttonVariants };Dependencies
Install
Item button-base-phosphor · registry JSON
npx shadcn@latest add https://labs.designfrom.com/r/button-base-phosphor.jsonnpx shadcn@latest add @designfrom/button-base-phosphorOnce, in components.json: "registries": { "@designfrom": "https://labs.designfrom.com/r/{name}.json" }
npx designfrom add button --primitive base --icons phosphornpm i @designfrom/reactimport { Button, buttonVariants } from "@designfrom/react/base/button/phosphor";Source
Comments are kept. The header lists provenance, license and install commands for people and coding assistants.
import { Button as ButtonPrimitive } from "@base-ui/react/button";
import { cva, type VariantProps } from "class-variance-authority";
import { CircleNotchIcon } from "@phosphor-icons/react/ssr";
import { cn } from "@/lib/utils";
/**
* @dfl-header: v1, generated by labs.designfrom.com at build time
* Item: button-base-phosphor (Button: Base UI, Phosphor)
* What: A button with five variants, four sizes and a loading state that
* keeps focus and announces progress, on Radix, Base UI or React Aria.
* Status: ready. License: MIT. Author: Design From Labs
* <https://labs.designfrom.com>
* Source: https://github.com/Design-from-labs/labs @ 803b92c1a30e (examples/button)
* Credits: S-003 React (MIT); S-004 shadcn/ui and its registry schema (MIT);
* S-005 Lucide icons (ISC); S-006 Phosphor Icons (MIT); S-008
* Tailwind CSS (MIT); S-018 class-variance-authority (Apache-2.0);
* S-032 Radix Primitives (the unified radix-ui package) (MIT);
* S-033 Base UI (@base-ui/react) (MIT); S-034 React Aria Components
* (react-aria-components) (Apache-2.0)
* Install: npx shadcn@latest add https://labs.designfrom.com/r/button-base-phosphor.json
* npx shadcn@latest add @designfrom/button-base-phosphor
* npx designfrom add button --primitive base --icons phosphor
* npm i @designfrom/react, then import { Button, buttonVariants } from "@designfrom/react/base/button/phosphor";
* Requires: class-variance-authority, @base-ui/react, @phosphor-icons/react, registry: utils
* Docs: https://labs.designfrom.com/ui/button (index: https://labs.designfrom.com/llms.txt)
*/
// Every color comes from the shared token layer (primary, accent, ring...),
// so the button follows whatever theme the host project defines.
const buttonVariants = cva(
"inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm outline-none transition-[color,background-color,box-shadow] duration-150 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 aria-busy:cursor-progress motion-reduce:transition-none [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
ghost: "hover:bg-accent hover:text-accent-foreground",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
},
size: {
sm: "h-8 px-3 text-xs",
md: "h-9 px-4",
lg: "h-10 px-6",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "md",
},
},
);
export interface ButtonProps
extends Omit<ButtonPrimitive.Props, "className">,
VariantProps<typeof buttonVariants> {
className?: string;
/** Shows a spinner, sets aria-busy and disables the button while true. */
loading?: boolean;
}
function Button({
className,
variant,
size,
loading = false,
disabled,
children,
...props
}: ButtonProps) {
return (
<ButtonPrimitive
data-slot="button"
// Base UI keeps a disabled button focusable on request, so focus is
// not lost while the action runs.
disabled={disabled || loading}
focusableWhenDisabled={loading}
aria-busy={loading || undefined}
className={cn(buttonVariants({ variant, size }), className)}
{...props}
>
{loading ? (
<CircleNotchIcon aria-hidden="true" className="motion-safe:animate-spin" />
) : null}
{children}
</ButtonPrimitive>
);
}
export { Button, buttonVariants };Dependencies
Install
Item button-aria-lucide · registry JSON
npx shadcn@latest add https://labs.designfrom.com/r/button-aria-lucide.jsonnpx shadcn@latest add @designfrom/button-aria-lucideOnce, in components.json: "registries": { "@designfrom": "https://labs.designfrom.com/r/{name}.json" }
npx designfrom add button --primitive arianpm i @designfrom/reactimport { Button, buttonVariants } from "@designfrom/react/aria/button";Source
Comments are kept. The header lists provenance, license and install commands for people and coding assistants.
"use client";
import { cva, type VariantProps } from "class-variance-authority";
import { LoaderCircle } from "lucide-react";
import type * as React from "react";
import {
Button as ButtonPrimitive,
type ButtonProps as ButtonPrimitiveProps,
} from "react-aria-components";
import { cn } from "@/lib/utils";
/**
* @dfl-header: v1, generated by labs.designfrom.com at build time
* Item: button-aria-lucide (Button: React Aria, Lucide)
* What: A button with five variants, four sizes and a loading state that
* keeps focus and announces progress, on Radix, Base UI or React Aria.
* Status: ready. License: MIT. Author: Design From Labs
* <https://labs.designfrom.com>
* Source: https://github.com/Design-from-labs/labs @ 803b92c1a30e (examples/button)
* Credits: S-003 React (MIT); S-004 shadcn/ui and its registry schema (MIT);
* S-005 Lucide icons (ISC); S-006 Phosphor Icons (MIT); S-008
* Tailwind CSS (MIT); S-018 class-variance-authority (Apache-2.0);
* S-032 Radix Primitives (the unified radix-ui package) (MIT);
* S-033 Base UI (@base-ui/react) (MIT); S-034 React Aria Components
* (react-aria-components) (Apache-2.0)
* Install: npx shadcn@latest add https://labs.designfrom.com/r/button-aria-lucide.json
* npx shadcn@latest add @designfrom/button-aria-lucide
* npx designfrom add button --primitive aria
* npm i @designfrom/react, then import { Button, buttonVariants } from "@designfrom/react/aria/button";
* Requires: class-variance-authority, react-aria-components, lucide-react, registry: utils
* Docs: https://labs.designfrom.com/ui/button (index: https://labs.designfrom.com/llms.txt)
*/
// Every color comes from the shared token layer (primary, accent, ring...),
// so the button follows whatever theme the host project defines.
const buttonVariants = cva(
"inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm outline-none transition-[color,background-color,box-shadow] duration-150 data-focus-visible:ring-2 data-focus-visible:ring-ring data-focus-visible:ring-offset-2 data-focus-visible:ring-offset-background data-disabled:pointer-events-none data-disabled:opacity-50 data-pending:cursor-progress motion-reduce:transition-none [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
ghost: "hover:bg-accent hover:text-accent-foreground",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
},
size: {
sm: "h-8 px-3 text-xs",
md: "h-9 px-4",
lg: "h-10 px-6",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "md",
},
},
);
export interface ButtonProps
extends Omit<ButtonPrimitiveProps, "className" | "children">,
VariantProps<typeof buttonVariants> {
className?: string;
children?: React.ReactNode;
/**
* Shows a spinner while true. Maps to React Aria's isPending, which keeps
* focus, ignores presses and announces the pending state.
*/
loading?: boolean;
}
function Button({
className,
variant,
size,
loading = false,
isPending,
children,
...props
}: ButtonProps) {
const pending = loading || isPending;
return (
<ButtonPrimitive
data-slot="button"
isPending={pending}
className={cn(buttonVariants({ variant, size }), className)}
{...props}
>
{pending ? (
<LoaderCircle aria-hidden="true" className="motion-safe:animate-spin" />
) : null}
{children}
</ButtonPrimitive>
);
}
export { Button, buttonVariants };Dependencies
Install
Item button-aria-phosphor · registry JSON
npx shadcn@latest add https://labs.designfrom.com/r/button-aria-phosphor.jsonnpx shadcn@latest add @designfrom/button-aria-phosphorOnce, in components.json: "registries": { "@designfrom": "https://labs.designfrom.com/r/{name}.json" }
npx designfrom add button --primitive aria --icons phosphornpm i @designfrom/reactimport { Button, buttonVariants } from "@designfrom/react/aria/button/phosphor";Source
Comments are kept. The header lists provenance, license and install commands for people and coding assistants.
"use client";
import { cva, type VariantProps } from "class-variance-authority";
import { CircleNotchIcon } from "@phosphor-icons/react/ssr";
import type * as React from "react";
import {
Button as ButtonPrimitive,
type ButtonProps as ButtonPrimitiveProps,
} from "react-aria-components";
import { cn } from "@/lib/utils";
/**
* @dfl-header: v1, generated by labs.designfrom.com at build time
* Item: button-aria-phosphor (Button: React Aria, Phosphor)
* What: A button with five variants, four sizes and a loading state that
* keeps focus and announces progress, on Radix, Base UI or React Aria.
* Status: ready. License: MIT. Author: Design From Labs
* <https://labs.designfrom.com>
* Source: https://github.com/Design-from-labs/labs @ 803b92c1a30e (examples/button)
* Credits: S-003 React (MIT); S-004 shadcn/ui and its registry schema (MIT);
* S-005 Lucide icons (ISC); S-006 Phosphor Icons (MIT); S-008
* Tailwind CSS (MIT); S-018 class-variance-authority (Apache-2.0);
* S-032 Radix Primitives (the unified radix-ui package) (MIT);
* S-033 Base UI (@base-ui/react) (MIT); S-034 React Aria Components
* (react-aria-components) (Apache-2.0)
* Install: npx shadcn@latest add https://labs.designfrom.com/r/button-aria-phosphor.json
* npx shadcn@latest add @designfrom/button-aria-phosphor
* npx designfrom add button --primitive aria --icons phosphor
* npm i @designfrom/react, then import { Button, buttonVariants } from "@designfrom/react/aria/button/phosphor";
* Requires: class-variance-authority, react-aria-components, @phosphor-icons/react, registry: utils
* Docs: https://labs.designfrom.com/ui/button (index: https://labs.designfrom.com/llms.txt)
*/
// Every color comes from the shared token layer (primary, accent, ring...),
// so the button follows whatever theme the host project defines.
const buttonVariants = cva(
"inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm outline-none transition-[color,background-color,box-shadow] duration-150 data-focus-visible:ring-2 data-focus-visible:ring-ring data-focus-visible:ring-offset-2 data-focus-visible:ring-offset-background data-disabled:pointer-events-none data-disabled:opacity-50 data-pending:cursor-progress motion-reduce:transition-none [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
ghost: "hover:bg-accent hover:text-accent-foreground",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
},
size: {
sm: "h-8 px-3 text-xs",
md: "h-9 px-4",
lg: "h-10 px-6",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "md",
},
},
);
export interface ButtonProps
extends Omit<ButtonPrimitiveProps, "className" | "children">,
VariantProps<typeof buttonVariants> {
className?: string;
children?: React.ReactNode;
/**
* Shows a spinner while true. Maps to React Aria's isPending, which keeps
* focus, ignores presses and announces the pending state.
*/
loading?: boolean;
}
function Button({
className,
variant,
size,
loading = false,
isPending,
children,
...props
}: ButtonProps) {
const pending = loading || isPending;
return (
<ButtonPrimitive
data-slot="button"
isPending={pending}
className={cn(buttonVariants({ variant, size }), className)}
{...props}
>
{pending ? (
<CircleNotchIcon aria-hidden="true" className="motion-safe:animate-spin" />
) : null}
{children}
</ButtonPrimitive>
);
}
export { Button, buttonVariants };Dependencies
Usage
<Button onClick={save} loading={saving}>Save changes</Button>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | "default" | "secondary" | "outline" | "ghost" | "destructive" | "default" | Visual weight. Use one default button per view. |
| size | "sm" | "md" | "lg" | "icon" | "md" | Height and padding. An icon button needs an aria-label. |
| loading | boolean | false | Shows a spinner and blocks activation while keeping focus: aria-disabled on Radix, focusableWhenDisabled on Base UI, isPending on React Aria. |
| asChild | boolean | false | Radix only: render the child element (for example a link) with button styles. |
| onPress | (e: PressEvent) => void | React Aria only: use onPress instead of onClick. | |
| ...props | React.ComponentProps<"button"> | Every native button attribute. |
Accessibility
- A native button element: Enter and Space activate it, and it is in the tab order.
- Focus is always visible with a two-pixel ring in the ring token color.
- While loading, focus stays on the button: Radix uses aria-disabled with a click guard, Base UI keeps a disabled button focusable, React Aria uses isPending, which also announces the pending state.
- The spinner is aria-hidden and only spins when the user allows motion (motion-safe).
Credits
- React · Meta Platforms and contributors · MIT
- shadcn/ui and its registry schema · shadcn · MIT
- Lucide icons · Lucide contributors · ISC
- Phosphor Icons · Phosphor Icons (Helena Zhang and Tobias Fried) · MIT
- Tailwind CSS · Tailwind Labs · MIT
- class-variance-authority · Joe Bell · Apache-2.0
- Radix Primitives (the unified radix-ui package) · WorkOS and contributors · MIT
- Base UI (@base-ui/react) · MUI and contributors · MIT
- React Aria Components (react-aria-components) · Adobe and contributors · Apache-2.0
License and source
- License
- MIT
- Author
- Design From Labs <https://labs.designfrom.com>
- Source
- github.com/Design-from-labs/labs
- Commit
- 803b92c1a30e
- Date
- 2026-10-12
- Origin
- labs-examples