Round-up donation

Opt-in round-up row that computes the cents to the next dollar and names the cause.

componentswitchanydonation
$npx shadcn@latest add @storekit/cart-donation
cart-donation

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-donation
$pnpm dlx shadcn@latest add @storekit/cart-donation
$yarn dlx shadcn@latest add @storekit/cart-donation
$bunx --bun shadcn@latest add @storekit/cart-donation

Source

components/storekit/cart/cart-donation.tsx
72 lines
"use client"

import * as React from "react"
import { motion, useReducedMotion } from "motion/react"
import { HeartHandshake } from "lucide-react"
import { cn, money } from "@/components/storekit/lib/cn"
import { useSkBlock } from "@/components/storekit/lib/sk-analytics"

/* Round-up donation row. Computes the cents needed to reach the next whole
   dollar (a full dollar if the total is already round), names the cause,
   and stays off until the shopper turns it on. */

export function CartDonation({
  total = 14735,
  cause = "Open Science Fund",
  detail = "Funds school garden programs near where we source. Every cent is passed on and listed on your receipt.",
  defaultOn = false,
  onChange,
  className,
}: {
  /** Order total in cents before the round-up. */
  total?: number
  cause?: string
  detail?: string
  defaultOn?: boolean
  onChange?: (on: boolean, amount: number) => void
  className?: string
}) {
  const reduce = useReducedMotion()
  const sk = useSkBlock("cart", "cart-donation")
  const [on, setOn] = React.useState(defaultOn)
  const amount = total % 100 === 0 ? 100 : 100 - (total % 100)
  const rounded = total + amount

  return (
    <div ref={sk.ref as React.Ref<HTMLDivElement>} {...sk.attrs} className={cn("w-full rounded-sk-card border border-sk-line bg-sk-card", className)}>
      <button
        type="button"
        role="switch"
        aria-checked={on}
        onClick={() => {
          setOn(!on)
          onChange?.(!on, amount)
          sk.emit(on ? "interact" : "convert", on ? "donation_off" : "donation_on", amount)
        }}
        className="flex min-h-[64px] w-full items-start gap-3 rounded-sk-card p-4 text-left transition-colors duration-200 hover:bg-sk-ink/[0.03] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-sk-accent active:scale-[0.99]"
      >
        <span className={cn("grid h-9 w-9 shrink-0 place-items-center rounded-sk-chip transition-colors duration-200", on ? "bg-sk-accent/15 text-sk-accent" : "bg-sk-ink/[0.06] text-sk-muted")}>
          <HeartHandshake size={16} />
        </span>
        <span className="min-w-0 flex-1">
          <span className="block text-sm font-medium text-sk-ink">
            Round up to <span className="tabular-nums">{money(rounded)}</span>
          </span>
          <span className="block text-[13px] text-sk-muted">
            <span className="tabular-nums">{money(amount)}</span> to the {cause}
          </span>
          <span className="mt-1 block text-xs leading-[1.5] text-sk-muted">{detail}</span>
        </span>
        <span aria-hidden className={cn("relative mt-1.5 flex h-6 w-10 shrink-0 items-center rounded-sk-chip p-0.5 transition-colors duration-200", on ? "bg-sk-accent" : "bg-sk-ink/20")}>
          <motion.span
            initial={false}
            animate={{ x: on ? 16 : 0 }}
            transition={reduce ? { duration: 0 } : { type: "spring", stiffness: 600, damping: 34 }}
            className={cn("h-5 w-5 rounded-sk-chip shadow-[0_1px_2px_rgb(0_0_0/0.25)]", on ? "bg-sk-accent-ink" : "bg-sk-paper")}
          />
        </span>
      </button>
    </div>
  )
}

Usage

Import from "@/components/storekit/cart/cart-donation" and pass your product data. The demo above exercises the full prop surface with sample values.

Round-up donation, StoreKit