Page Sections

Sketchnote Pricing

A notebook-paper pricing section for React: hand-drawn borders and check marks stroke in, the recommended plan gets a sketched circle and note, and prices tween between monthly and yearly.

A pricing section that looks like an open notebook. Each plan card is ruled notebook paper — faint blue-grey lines, a red margin line on the left and three binder holes along the top. There is no CSS border: a jittered SVG path is drawn to the card's measured size and strokes in on entry, then each feature check draws in, and finally the sketched loop, arrow and handwritten note around the recommended plan appear. Above the cards, a sticky-note style monthly/yearly toggle slides a hand-drawn circle under the selected option; on change, prices tween while the old price floats up, gets struck through and fades, and in yearly mode the struck-out full price sits alongside. The strokes come from seed, so the same seed always renders the same drawing and SSR never mismatches the browser.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add sketchnote-pricing.en

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

5
260
Active edited usageDefault props
<SketchnotePricing/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add https://webberui.com/r/sketchnote-pricing.en.json

Or, once registries are configured in components.json, install it as @webberui/sketchnote-pricing.

Usage

import { SketchnotePricing } from "@/components/ui/sketchnote-pricing";

<SketchnotePricing
  eyebrow="方案"
  title="挑一本適合你的筆記本"
  subtitle="年繳等於送你兩個月。"
  plans={[
    {
      id: "starter",
      name: "入門",
      price: { monthly: 0, yearly: 0 },
      features: [
        { label: "每月 3 堂公開課", included: true },
        { label: "完整課程庫", included: false },
      ],
      cta: { label: "免費開始", href: "/signup" },
    },
    {
      id: "plus",
      name: "進階",
      price: { monthly: 290, yearly: 2900 },
      features: [
        { label: "完整課程庫", included: true },
        { label: "講師問答", included: true },
      ],
      cta: { label: "升級進階", onClick: () => console.log("upgrade") },
      recommended: true,
      note: "最多人選",
    },
  ]}
  onBillingChange={(billing) => console.log(billing)}
/>

Props

PropTypeDefaultDescription
plansSketchnotePricingPlan[]—Plans, three recommended; stacked on narrow screens, three columns from md up
billing"monthly" | "yearly"—Controlled billing cycle; managed internally when omitted
defaultBilling"monthly" | "yearly""monthly"Initial billing cycle in uncontrolled mode
onBillingChange(billing: "monthly" | "yearly") => void—Called when the billing cycle changes
currencystring"NT$"Currency prefix before prices
yearlyNotestring"省 2 個月" (2 months free)Handwritten note beside the toggle; pass an empty string to hide it
eyebrowReactNode—Small label above the title
titleReactNode—Section title with a hand-drawn wavy underline; used as the section's aria-labelledby when provided
subtitleReactNode—Subtitle
seednumber5Seed for the hand-drawn jitter; the same value always draws the same strokes
grainbooleanfalseOverlay feTurbulence grain on the paper
classNamestring—Passed to the outer section

SketchnotePricingPlan

FieldTypeDefaultDescription
idstring—Unique plan id
namestring—Plan name (slightly tilted, like a handwritten heading)
descriptionstring—Short plan description
price{ monthly: number; yearly: number }—monthly is the per-month amount, yearly the annual total; when yearly is below 12 months the struck-out full price is shown
features{ label: string; included: boolean }[]—Feature list; included: false renders faded with a hand-drawn strike-through
cta{ label: string; href?: string; onClick?: () => void }—Call-to-action; renders a link with href, otherwise a button
recommendedbooleanfalseRecommended plan: tilted, slightly larger, with a sketched loop and arrow note
notestring"最多人選" (Most popular)Handwritten note beside the recommended plan

CSS variables

VariableDefaultDescription
--wb-sketchnote-pricing-accent#0f766eAccent: checks, loop, arrow, underline, recommended button
--wb-sketchnote-pricing-accent-dark#2dd4bfAccent in dark mode
--wb-sketchnote-pricing-paper#fffdf5Notebook paper color
--wb-sketchnote-pricing-paper-dark#171717Paper color in dark mode (neutral-900); the rules dim as well
--wb-sketchnote-pricing-radius14pxCard radius; the hand-drawn border turns its corners at this value
--wb-sketchnote-pricing-duration260msDraw time unit: card frame × 3, each check × 1, recommended loop × 3

How it works

  • Entry order: once the section scrolls into view, card frames draw pathLength from 0 to 1 (unit × 3, 120ms between cards), then each feature check draws in (unit × 1, 60ms apart), and only after all of that the recommended loop and arrow; every draw uses cubic-bezier(0.65, 0, 0.35, 1).
  • The hand-drawn border is redrawn to the card's real size via ResizeObserver, with a ±1.5px perpendicular jitter roughly every 48px and an end that overshoots the start like an unfinished pen stroke. Every card and every check derives its own strokes from seed plus its index.
  • Hovering a card lifts it 4px on a spring (stiffness 300, damping 20); the recommended card straightens out while the others tilt ±0.6°.
  • The time unit is read from --wb-sketchnote-pricing-duration on mount, so a CSS override also drives the JS draw animations; changing the variable at runtime takes effect after a remount.

Accessibility

  • The outer element is a section labelled by its title via aria-labelledby; each plan card is an article labelled by its plan name
  • The monthly/yearly toggle is a role="radiogroup" of role="radio" options with aria-checked; it uses a roving tabindex, and arrow keys, Home and End switch options
  • Besides the visual loop, the recommended plan carries a screen-reader-only "recommended plan" label; excluded features are prefixed with "not included"
  • Price changes announce the final price and original price via aria-live="polite"; the tweening digits and all hand-drawn SVGs are hidden from assistive tech
  • When the user's system has reduce motion enabled, every stroke renders complete immediately, prices switch without tweening, and the card hover lift is disabled