Express checkout
Three wallet buttons as mono text marks, an or divider and a short trust row.
componentexpressanycheckoutexpress-paytrust
$
npx shadcn@latest add @storekit/checkout-expresscheckout-express
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/checkout-express$
pnpm dlx shadcn@latest add @storekit/checkout-express$
yarn dlx shadcn@latest add @storekit/checkout-express$
bunx --bun shadcn@latest add @storekit/checkout-expressSource
components/storekit/checkout/checkout-express.tsx
90 lines
"use client"
import * as React from "react"
import { RotateCcw, ShieldCheck, Truck } from "lucide-react"
import { cn } from "@/components/storekit/lib/cn"
import { useSkBlock } from "@/components/storekit/lib/sk-analytics"
/* Express pay row. The first wallet is the filled primary, the rest are
bordered text marks (swap in each provider's official button in
production), then an "or" divider and a three-up trust row. */
type Wallet = { id: string; label: string }
const WALLETS: Wallet[] = [
{ id: "express", label: "Express pay" },
{ id: "shop", label: "Shop Pay" },
{ id: "paypal", label: "PayPal" },
]
const TRUST = [
{ icon: ShieldCheck, label: "Encrypted checkout" },
{ icon: Truck, label: "Ships in 24 hours" },
{ icon: RotateCcw, label: "30-day returns" },
]
export function CheckoutExpress({
wallets = WALLETS,
onSelect,
className,
}: {
wallets?: Wallet[]
onSelect?: (id: string) => void
className?: string
}) {
const sk = useSkBlock("checkout", "checkout-express")
const [pending, setPending] = React.useState<string | null>(null)
const pick = (id: string) => {
sk.emit("convert", `express_${id}`)
setPending(id)
window.setTimeout(() => {
setPending(null)
onSelect?.(id)
}, 900)
}
return (
<div ref={sk.ref as React.Ref<HTMLDivElement>} {...sk.attrs} className={cn("w-full", className)}>
<p className="text-center font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-muted">Express checkout</p>
<div className="mt-3 grid gap-2 sm:grid-cols-3">
{wallets.map((w, i) => (
<button
key={w.id}
type="button"
onClick={() => pick(w.id)}
disabled={pending !== null}
aria-busy={pending === w.id || undefined}
className={cn(
"relative h-12 rounded-sk-btn border text-sm font-semibold tracking-[-0.01em]",
"transition-[transform,background-color,opacity] duration-200 active:scale-[0.97] disabled:opacity-60",
i === 0 ? "border-sk-ink bg-sk-ink text-sk-paper hover:bg-sk-ink/90" : "border-sk-line bg-sk-card text-sk-ink hover:bg-sk-ink/[0.04]",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sk-accent focus-visible:ring-offset-2 focus-visible:ring-offset-sk-paper"
)}
>
<span className={cn(pending === w.id && "opacity-0")}>{w.label}</span>
{pending === w.id ? (
<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}
</button>
))}
</div>
<div className="my-6 flex items-center gap-4" role="separator" aria-label="or">
<span className="h-px flex-1 bg-sk-line" />
<span className="text-[13px] text-sk-muted">or</span>
<span className="h-px flex-1 bg-sk-line" />
</div>
<ul className="grid grid-cols-3 gap-2">
{TRUST.map(({ icon: Icon, label }) => (
<li key={label} className="flex flex-col items-center gap-1.5 text-center text-xs text-sk-muted sm:flex-row sm:justify-center sm:text-left">
<Icon size={16} className="shrink-0 text-sk-ink" strokeWidth={1.75} />
{label}
</li>
))}
</ul>
</div>
)
}
Usage
Text marks are placeholders. Use each provider's official button in production.