Components / Primitives
Button
Free · MITA button with six variants, three sizes, a pill shape and a built-in loading state. Renders any element with asChild.
v1.0.0 · Updated 2026-09-28
import { ArrowRight } from "lucide-react"
import { Button } from "@/components/ballmac/button"
export default function ButtonDemo() {
return (
<Button>
Get started <ArrowRight />
</Button>
)
}01Installation
$ pnpm dlx shadcn@latest add @ballmac/buttonInstall manually
Install the dependencies:
$ pnpm add radix-ui class-variance-authorityCopy the source into your project:
components/ballmac/button.tsx
// Ballmac UI: Button. https://ui.ballmac.com/components/button
// Based on shadcn/ui's Button (MIT, Copyright (c) 2023 shadcn), restyled and extended.
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"relative inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-[color,background-color,border-color,box-shadow,transform] duration-150 select-none active:translate-y-px disabled:pointer-events-none disabled:opacity-50 focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background aria-invalid:ring-destructive/30 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-[inset_0_1px_0_0_rgb(255_255_255/0.12),0_1px_2px_0_rgb(0_0_0/0.12)] hover:bg-primary/90",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
outline:
"border border-border bg-background shadow-xs hover:border-foreground/20 hover:bg-accent hover:text-accent-foreground",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-foreground underline-offset-4 hover:underline",
destructive: "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/40",
},
size: {
sm: "h-8 gap-1.5 px-3 text-[13px]",
default: "h-9 px-4",
lg: "h-11 px-6 text-[15px]",
icon: "size-9",
"icon-sm": "size-8",
},
shape: {
default: "",
pill: "rounded-full",
},
},
defaultVariants: { variant: "default", size: "default", shape: "default" },
}
)
type ButtonProps = React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
/** Render the child element (for example a link) with button styles. */
asChild?: boolean
/** Shows a spinner, sets aria-busy and blocks clicks. */
loading?: boolean
}
function Button({
className,
variant,
size,
shape,
asChild = false,
loading = false,
disabled,
children,
...props
}: ButtonProps) {
const Comp = asChild ? Slot.Root : "button"
return (
<Comp
data-slot="button"
data-loading={loading || undefined}
aria-busy={loading || undefined}
disabled={asChild ? undefined : disabled || loading}
className={cn(buttonVariants({ variant, size, shape }), className)}
{...props}
>
{loading && !asChild ? (
<>
<span className="absolute inset-0 flex items-center justify-center" aria-hidden="true">
<svg className="size-4 animate-spin" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="9" stroke="currentColor" strokeOpacity="0.25" strokeWidth="3" />
<path d="M21 12a9 9 0 0 0-9-9" stroke="currentColor" strokeWidth="3" strokeLinecap="round" />
</svg>
</span>
<span className="invisible inline-flex items-center gap-2">{children}</span>
</>
) : (
children
)}
</Comp>
)
}
export { Button, buttonVariants, type ButtonProps }02Usage
import { Button } from "@/components/ballmac/button"03Examples
Variants
import { Button } from "@/components/ballmac/button"
export default function ButtonVariants() {
return (
<div className="flex flex-wrap items-center justify-center gap-3">
<Button>Default</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
<Button variant="destructive">Delete</Button>
<Button shape="pill">Pill</Button>
</div>
)
}Loading
"use client"
import * as React from "react"
import { Button } from "@/components/ballmac/button"
export default function ButtonLoading() {
const [loading, setLoading] = React.useState(false)
return (
<Button
loading={loading}
onClick={() => {
setLoading(true)
setTimeout(() => setLoading(false), 1600)
}}
>
Save changes
</Button>
)
}04Options
- variant: default | secondary | outline | ghost | link | destructive
- size: sm | default | lg | icon | icon-sm
- shape: default | pill
05Accessibility
| Key | Action |
|---|---|
| Enter / Space | Activates the button |
| Tab | Moves focus to the button |
06Dependencies
- Registry
- shadcn/utils
07Use with AI
The base action control. Use variant for emphasis, size for density, and loading while an async action runs. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Button (@ballmac/button) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Primary and secondary actions
- Form submit buttons
- Links that should look like buttons (asChild with <a>)
Not for
- Navigation inside running text (use a link)
- Toggling state (use a switch or toggle)
Registry JSON: https://ui.ballmac.com/r/button.json
08Source
components/ballmac/button.tsx
// Ballmac UI: Button. https://ui.ballmac.com/components/button
// Based on shadcn/ui's Button (MIT, Copyright (c) 2023 shadcn), restyled and extended.
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"relative inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-[color,background-color,border-color,box-shadow,transform] duration-150 select-none active:translate-y-px disabled:pointer-events-none disabled:opacity-50 focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background aria-invalid:ring-destructive/30 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-[inset_0_1px_0_0_rgb(255_255_255/0.12),0_1px_2px_0_rgb(0_0_0/0.12)] hover:bg-primary/90",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
outline:
"border border-border bg-background shadow-xs hover:border-foreground/20 hover:bg-accent hover:text-accent-foreground",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-foreground underline-offset-4 hover:underline",
destructive: "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/40",
},
size: {
sm: "h-8 gap-1.5 px-3 text-[13px]",
default: "h-9 px-4",
lg: "h-11 px-6 text-[15px]",
icon: "size-9",
"icon-sm": "size-8",
},
shape: {
default: "",
pill: "rounded-full",
},
},
defaultVariants: { variant: "default", size: "default", shape: "default" },
}
)
type ButtonProps = React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
/** Render the child element (for example a link) with button styles. */
asChild?: boolean
/** Shows a spinner, sets aria-busy and blocks clicks. */
loading?: boolean
}
function Button({
className,
variant,
size,
shape,
asChild = false,
loading = false,
disabled,
children,
...props
}: ButtonProps) {
const Comp = asChild ? Slot.Root : "button"
return (
<Comp
data-slot="button"
data-loading={loading || undefined}
aria-busy={loading || undefined}
disabled={asChild ? undefined : disabled || loading}
className={cn(buttonVariants({ variant, size, shape }), className)}
{...props}
>
{loading && !asChild ? (
<>
<span className="absolute inset-0 flex items-center justify-center" aria-hidden="true">
<svg className="size-4 animate-spin" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="9" stroke="currentColor" strokeOpacity="0.25" strokeWidth="3" />
<path d="M21 12a9 9 0 0 0-9-9" stroke="currentColor" strokeWidth="3" strokeLinecap="round" />
</svg>
</span>
<span className="invisible inline-flex items-center gap-2">{children}</span>
</>
) : (
children
)}
</Comp>
)
}
export { Button, buttonVariants, type ButtonProps }Based on shadcn/ui Button (MIT, Copyright (c) 2023 shadcn), modified by Ballmac. Released under the MIT License.