Button
Solid, outline, ghost and ink variants. Loading state, 2% press scale, visible focus ring.
ui
$
npx shadcn@latest add @storekit/buttonbutton
Live and interactive. Switch the brand in the frame header to see the same source under another token file.
Installation
$
npx shadcn@latest add @storekit/button$
pnpm dlx shadcn@latest add @storekit/button$
yarn dlx shadcn@latest add @storekit/button$
bunx --bun shadcn@latest add @storekit/buttonSource
components/storekit/ui/button.tsx
66 lines
"use client"
import * as React from "react"
import { cn } from "@/components/storekit/lib/cn"
/* StoreKit Button. Reads only sk-* tokens: accent fill, accent-ink text,
btn radius. Press feedback is a 2% scale on :active, nothing bouncier.
Inline buttons never wrap. A full-width button (w-full) may grow to two
balanced lines instead of overflowing, so long labels in uppercase,
wide-tracked styles stay inside the button on narrow phones. */
type Variant = "solid" | "outline" | "ghost" | "ink"
type Size = "sm" | "md" | "lg" | "icon"
const variants: Record<Variant, string> = {
solid: "bg-sk-accent text-sk-accent-ink hover:brightness-[1.06] active:brightness-95",
outline: "border border-sk-line bg-transparent text-sk-ink hover:bg-sk-ink/[0.04]",
ghost: "bg-transparent text-sk-ink hover:bg-sk-ink/[0.05]",
ink: "bg-sk-ink text-sk-paper hover:opacity-90",
}
const sizes: Record<Size, string> = {
sm: "h-10 px-4 text-[13px] [&.w-full]:min-h-10 [&.w-full]:py-2",
md: "h-11 px-5 text-sm [&.w-full]:min-h-11 [&.w-full]:py-2",
lg: "h-12 px-7 text-[15px] [&.w-full]:min-h-12 [&.w-full]:py-2.5",
icon: "h-10 w-10",
}
const fullWidthWrap =
"[&.w-full]:h-auto [&.w-full]:whitespace-normal [&.w-full]:text-center [&.w-full]:leading-tight [&.w-full>span]:flex-wrap [&.w-full>span]:justify-center [&.w-full>span]:gap-y-0.5"
export type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: Variant
size?: Size
loading?: boolean
}
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function Button(
{ className, variant = "solid", size = "md", loading, disabled, children, ...props },
ref
) {
return (
<button
ref={ref}
disabled={disabled || loading}
aria-busy={loading || undefined}
className={cn(
"sk-btn relative inline-flex select-none items-center justify-center gap-2 whitespace-nowrap rounded-sk-btn font-medium outline-none transition-[transform,background-color,color,opacity,filter] duration-200 ease-sk-out",
"focus-visible:ring-2 focus-visible:ring-sk-accent focus-visible:ring-offset-2 focus-visible:ring-offset-sk-paper",
"active:scale-[0.98] disabled:pointer-events-none disabled:opacity-50",
variants[variant],
sizes[size],
fullWidthWrap,
className
)}
{...props}
>
{loading ? (
<span className="absolute inset-0 grid place-items-center">
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
</span>
) : null}
<span className={cn("inline-flex items-center gap-2", loading && "opacity-0")}>{children}</span>
</button>
)
})
Usage
Import from "@/components/storekit/ui/button" and pass your product data. The demo above exercises the full prop surface with sample values.