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.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<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.jsonOr, 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
| Prop | Type | Default | Description |
|---|---|---|---|
plans | SketchnotePricingPlan[] | — | 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 |
currency | string | "NT$" | Currency prefix before prices |
yearlyNote | string | "省 2 個月" (2 months free) | Handwritten note beside the toggle; pass an empty string to hide it |
eyebrow | ReactNode | — | Small label above the title |
title | ReactNode | — | Section title with a hand-drawn wavy underline; used as the section's aria-labelledby when provided |
subtitle | ReactNode | — | Subtitle |
seed | number | 5 | Seed for the hand-drawn jitter; the same value always draws the same strokes |
grain | boolean | false | Overlay feTurbulence grain on the paper |
className | string | — | Passed to the outer section |
SketchnotePricingPlan
| Field | Type | Default | Description |
|---|---|---|---|
id | string | — | Unique plan id |
name | string | — | Plan name (slightly tilted, like a handwritten heading) |
description | string | — | 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 |
recommended | boolean | false | Recommended plan: tilted, slightly larger, with a sketched loop and arrow note |
note | string | "最多人選" (Most popular) | Handwritten note beside the recommended plan |
CSS variables
| Variable | Default | Description |
|---|---|---|
--wb-sketchnote-pricing-accent | #0f766e | Accent: checks, loop, arrow, underline, recommended button |
--wb-sketchnote-pricing-accent-dark | #2dd4bf | Accent in dark mode |
--wb-sketchnote-pricing-paper | #fffdf5 | Notebook paper color |
--wb-sketchnote-pricing-paper-dark | #171717 | Paper color in dark mode (neutral-900); the rules dim as well |
--wb-sketchnote-pricing-radius | 14px | Card radius; the hand-drawn border turns its corners at this value |
--wb-sketchnote-pricing-duration | 260ms | Draw 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
pathLengthfrom 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 usescubic-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 fromseedplus 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-durationon 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
sectionlabelled by its title viaaria-labelledby; each plan card is anarticlelabelled by its plan name - The monthly/yearly toggle is a
role="radiogroup"ofrole="radio"options witharia-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
Magnetic Lattice Hero
A dark tech-grid hero whose lattice of nodes and lines bends towards the pointer like a magnet, sending out a ripple wave from wherever you click.
Masonry Review Wall
A masonry wall of testimonial cards of varying heights, each column moving at its own parallax speed as you scroll over a slowly morphing generative backdrop, with a show-more expansion.