Free shipping bar
Progress toward a free-shipping threshold that computes the remainder and flips to a confirmation.
componentmeteranyshipping-threshold
$
npx shadcn@latest add @storekit/cart-shipping-barcart-shipping-bar
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/cart-shipping-bar$
pnpm dlx shadcn@latest add @storekit/cart-shipping-bar$
yarn dlx shadcn@latest add @storekit/cart-shipping-bar$
bunx --bun shadcn@latest add @storekit/cart-shipping-barSource
components/storekit/cart/cart-shipping-bar.tsx
68 lines
"use client"
import { motion, useReducedMotion } from "motion/react"
import { Check, Truck } from "lucide-react"
import { cn, money } from "@/components/storekit/lib/cn"
/* Free-shipping meter. Computes the remainder against a threshold, fills a
bar (scaleX, so only transform animates) and flips to the savings color with
a confirmation once the threshold is reached. */
export function CartShippingBar({
subtotal = 5200,
threshold = 7500,
className,
}: {
/** Cart subtotal in cents. */
subtotal?: number
/** Free-shipping threshold in cents. */
threshold?: number
className?: string
}) {
const reduce = useReducedMotion()
const remaining = Math.max(0, threshold - subtotal)
const unlocked = remaining === 0
const pct = Math.min(1, subtotal / threshold)
return (
<div data-sk-section="cart" data-sk-variant="cart-shipping-bar" className={cn("w-full", className)}>
<div className="flex items-center justify-between gap-3">
<p className="flex items-center gap-2 text-sm text-sk-ink" aria-live="polite">
<span
className={cn(
"grid h-7 w-7 place-items-center rounded-full transition-colors duration-300",
unlocked ? "bg-sk-savings/15 text-sk-savings" : "bg-sk-ink/[0.06] text-sk-muted"
)}
>
{unlocked ? <Check size={14} /> : <Truck size={14} />}
</span>
{unlocked ? (
<span className="font-medium text-sk-savings">Free shipping unlocked</span>
) : (
<span>
You are <span className="font-semibold tabular-nums">{money(remaining)}</span> away from free shipping
</span>
)}
</p>
<span className="hidden font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-muted tabular-nums sm:inline">
{money(threshold)}
</span>
</div>
<div
className="mt-3 h-1.5 overflow-hidden rounded-full bg-sk-ink/[0.08]"
role="progressbar"
aria-label="Progress to free shipping"
aria-valuemin={0}
aria-valuemax={threshold}
aria-valuenow={Math.min(subtotal, threshold)}
>
<motion.div
className={cn("h-full w-full origin-left rounded-full transition-colors duration-300", unlocked ? "bg-sk-savings" : "bg-sk-accent")}
initial={false}
animate={{ scaleX: pct }}
transition={{ duration: reduce ? 0 : 0.5, ease: [0.16, 1, 0.3, 1] }}
/>
</div>
</div>
)
}
Usage
Import from "@/components/storekit/cart/cart-shipping-bar" and pass your product data. The demo above exercises the full prop surface with sample values.