Button

Solid, outline, ghost and ink variants. Loading state, 2% press scale, visible focus ring.

ui
$npx shadcn@latest add @storekit/button
button

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/button

Source

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.

Button, StoreKit