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-bar
cart-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-bar

Source

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.

Free shipping bar, StoreKit