For dark mode, add the dark class to your <html> element:
Optional
<html class="dark">
Install with AI Canvas MCP
With AI Canvas MCP, your AI knows every component we ship. Ask for one inside Claude Code, Codex, or Cursor and it installs the component you pick. Works with any AI Canvas account, free or premium.
Every prop this component accepts. Any other prop is forwarded to the underlying element.
PricingTier
Prop
Type
Default
Description
namereq
string
-
Plan name shown as the card heading.
proreq
boolean
-
Whether this is the highlighted/hero plan.
monthlyreq
string
-
Price shown when billed monthly (e.g. "$19").
yearlyreq
string
-
Price shown when billed yearly (e.g. "$15").
suffixreq
string
-
Text after the price (e.g. "/month"); empty for a custom-quote plan.
ctareq
string
-
Call-to-action button label.
introreq
string
-
Line introducing the checklist (e.g. "What's included:").
featuresreq
string[]
-
Checklist rows shown under the CTA.
seatsreq
string
-
Footer band left stat (e.g. "5 Team Seats").
storagereq
string
-
Footer band right stat (e.g. "10 GB").
GemPricingSection
Prop
Type
Default
Description
tiers
PricingTier[]
DEFAULT_TIERS
The three plans shown left to right. Exactly one should have `pro: true`; the gem accessory and shape order pair by position (1st/2nd/3rd), so custom tiers keep the same three-plan shape.
onSelectPlan
(tierName: string) => void
-
Called with the plan name when a CTA button is clicked.
className
string
-
Extra classes merged onto the outermost root element.
Frequently asked questions
Is 3D Gem Pricing Section free to use?
3D Gem Pricing Section is a Premium AI Canvas component. Premium unlocks the full source code and one-command installs for every Premium component and template.
How do I install 3D Gem Pricing Section?
Premium unlocks one-command installs: run the shadcn CLI with your AI Canvas token, or connect the AI Canvas MCP server and ask your AI editor to install it for you. You can also recreate your own take on it with the included AI prompts.
What is 3D Gem Pricing Section built with?
3D Gem Pricing Section is built with React and TypeScript, using Pricing, Three.js, WebGL, and 3D. It ships with both light and dark styling.
Can I remix 3D Gem Pricing Section with AI?
Yes. 3D Gem Pricing Section ships with one comprehensive AI prompt written against the real source code. Open "Remix with AI" on this page to read and copy it into Claude, Cursor, ChatGPT, or any AI tool. Prompts are for remixing your own variation; for the exact component, install it with the one-command CLI.
Remix 3D Gem Pricing Section with AI
One comprehensive prompt, written against the real source code. Works in Claude, Cursor, ChatGPT, or any AI tool you use.
This prompt is for remixing. Use it to build your own variation of 3D Gem Pricing Section. Results depend on the model you use, and no prompt in the world is 100% exact.
Want the exact component?
One command installs it, pixel-perfect. Copy, paste into your project, done.
Build a "3D Gem Pricing Section" - a full three-card SaaS pricing section where each plan card carries its own faceted gem accessory at its top-right corner - as ONE self-contained, configurable React client component ('use client') using raw Three.js (dynamic import, NOT react-three-fiber) and @phosphor-icons/react.
At rest, each gem sits perfectly still as a small flat 2D dot glyph (no idle spin, no idle bob, ever). Hovering a card (tap on touch, keyboard focus on desktop) crossfades that card's glyph into a spinning, tilting 3D solid that grows to roughly 2.8x its rest size so it visibly overflows the card's top-right corner, then eases back down small and still on leave. The gem's GEOMETRY encodes the tier (more facets = higher tier); its color does not vary by tier, every gem shares the same single accent color at any given moment, only swapped between a dark-theme and light-theme palette.
PROP API (typed, not hardcoded): the default export takes three optional props so a buyer can plug in their own copy and behavior.
- `tiers?: PricingTier[]` - the three plans rendered left to right, defaulting to a built-in Basic / Pro / Enterprise array (below). Exactly one tier should carry `pro: true`; the gem shape and mobile reordering pair by ARRAY POSITION (1st = pyramid, 2nd = cube, 3rd = octahedron), not by which tier is marked pro, so a caller can swap in fully custom copy and still get the same three-gem visual language.
- `onSelectPlan?: (tierName: string) => void` - called with the tier's `name` when its CTA button is clicked. Optional; the button still renders and animates without it.
- `className?: string` - extra classes merged onto the outermost root element.
`PricingTier` shape: `{ name: string; pro: boolean; monthly: string; yearly: string; suffix: string; cta: string; intro: string; features: string[]; seats: string; storage: string }`. `monthly`/`yearly` are the two price strings swapped by the billing toggle; `suffix` is the small unit after the price (e.g. "/month", empty for a custom-quote plan); `cta` is the button label; `intro` introduces the checklist (e.g. "What's included:", "Everything in Basic, plus:"); `features` is the checklist row copy; `seats`/`storage` are the two footer-band stats.
Default `tiers` data (ship these exact three so the component works with zero props):
1. Basic Plan, pro: false, monthly "$19", yearly "$15", suffix "/month", cta "Start with Basic", intro "What's included:", features ["Up to 3 Projects", "Core Analytics Dashboard", "40+ Integrations", "Standard Email Support"], seats "5 Team Seats", storage "10 GB".
2. Pro Plan, pro: true, monthly "$59", yearly "$47", suffix "/month", cta "Start with Pro", intro "Everything in Basic, plus:", features ["Unlimited Projects", "Advanced Analytics & Reports", "Workflow Automations", "Priority Support"], seats "25 Team Seats", storage "100 GB".
3. Enterprise Plan, pro: false, monthly "Custom", yearly "Custom", suffix "", cta "Contact Sales", intro "Everything in Pro, plus:", features ["Unlimited Workspaces", "SSO & Advanced Security", "Custom Integrations", "Dedicated Success Manager"], seats "Unlimited Seats", storage "Unlimited".
VISUAL STYLE: warm near-black editorial page (#141312 dark theme, #F4EFE8 light theme), one single font family throughout, hairline low-alpha borders, uppercase micro-labels with wide letter-spacing, one burnt-orange accent (#E8552F dark / #D6421C light). Full dual-theme via Tailwind `dark:` variants. Root element: `min-h-screen w-full flex items-center justify-center`, background `bg-[#F4EFE8] dark:bg-[#141312]`, plus the caller's `className` merged in.
LAYOUT (a centered column, max-width 1120px, horizontal padding `px-4 py-8 sm:px-5 lg:px-8`):
1. Billing control, centered above the cards: one `button role="switch" aria-checked={yearly} aria-label="Billing period, yearly"` containing "MONTHLY" (12px uppercase, medium weight, 0.08em tracking, active/inactive text colors swap between #211B16/#F2EEE9 (dark) when selected and muted #6E655C/#9A938C when not), a 44x24px rounded pill in the accent color with an 18px cream/near-black knob that slides via `translate-x-5` over 200ms, then "YEARLY" with the same active/muted color logic. Clicking it flips a `yearly` boolean; there is no discount badge next to either label.
2. Three plan cards in a responsive grid: `grid-cols-1 gap-5 md:grid-cols-2 lg:grid-cols-3 lg:gap-6`, each card `mx-auto w-full max-w-[360px]`, `rounded-[16px]`, `overflow-hidden`, bordered, flex column, transitioning transform/box-shadow/border-color over 200ms. The Pro card always renders first when the grid collapses to one column on mobile (computed from the tier's position among `tiers`, via `tiers.findIndex(t => t.pro)`, not hardcoded to index 1, so a caller's reordered `tiers` array still puts Pro first on mobile and keeps natural left-to-right order at the `md` breakpoint and up).
- Non-pro card shell: `bg-[#FDFBF7] dark:bg-[#1B1917]`; hairline border `#1C161219` / `#FFFFFF12` at rest, tinting to the accent at 30% alpha plus a soft glow shadow while that card is hovered/focused/tapped active.
- Pro card shell: a vertical gradient from `#F3DCCF` to `#FBEFE7` (light) / `#3A221A` to `#241612` (dark), an accent-tinted border at all times, and an always-present soft orange glow shadow that grows slightly stronger while active. There is no separate "Most Popular" badge or chip anywhere on the card, the hero treatment is entirely the gradient, border and shadow.
- Card inner padding `px-6 pt-6`. Plan name: 20px semibold, `#211B16 dark:#F2EEE9`, no description line beneath it, the price follows directly.
- Price row (`mt-5`, `flex items-baseline gap-1`): the active price (`monthly` or `yearly` depending on the toggle) at 32px bold, `leading-none`, keyed by its own value so swapping it remounts a small span that plays an 180ms ease-out fade-and-rise-from-4px animation (declared via an injected `<style>` block with `@keyframes`, and disabled entirely under `prefers-reduced-motion: reduce` via a plain CSS media query, no JS branch needed for that part); the suffix (e.g. "/month") renders at 12px in a muted color and is omitted entirely when empty (the Enterprise tier).
- A dashed hairline divider (`border-t border-dashed`, `#1C161224 dark:#FFFFFF1A`).
- CTA button: full width, 44px tall, `rounded-[10px]`, 12px semibold uppercase with 0.12em tracking (the tier's `cta` string can be any case, the uppercase transform is CSS not data), `active:scale-95`, a focus-visible accent outline. Pro variant is solid: `bg-[#C23A18] text-[#FFF3EC] hover:bg-[#B33716]` light, `dark:bg-[#E8552F] dark:text-[#2B130B] dark:hover:bg-[#D6421C]`. Basic/Enterprise variant is quiet: a thin border, `bg-[#F4EFE8] text-[#342D27] hover:bg-[#EDE6DC]` light, `dark:border-[#FFFFFF1F] dark:bg-[#211E1B] dark:text-[#E8DFD6] dark:hover:bg-[#2A2622]` dark. Clicking it calls `onSelectPlan?.(tier.name)`.
- Checklist: a muted 14px medium intro line (the tier's `intro`), then a `ul` with one `li` per feature, each row an 18px rounded-square checkbox (bordered, faint tinted fill) holding a 12px Phosphor `Check` (accent-colored, slightly different tint on the Pro card) plus the 14px feature text in a shared muted-body color (`#57504A dark:#C4BDB4`) regardless of tier.
- Footer stat band, flush at the card's bottom edge (`mt-auto`, 44px tall, top hairline border, `px-6`, 12px medium): a Phosphor `Users` icon plus the tier's `seats` string on the left, a Phosphor `Database` icon plus `storage` on the right. Non-pro band: `bg-[#F1EAE1] text-[#6E655C]` light, `dark:bg-[#211E1B] dark:text-[#B5ADA3]` dark. Pro band: a more saturated `bg-[#EDC9B7] text-[#7A3F2A]` light, `dark:bg-[#45261B] dark:text-[#B5ADA3]` dark.
3. Below the grid, centered: a single 14px muted line, "Try all features free for 14 days. Upgrade anytime." Nothing else in that row, no trial CTA link.
THE 3D GEM OVERLAY (the signature mechanic, architecture is structural, do not deviate): one single `<div aria-hidden pointer-events-none>`, absolutely positioned `inset-0` with a z-index above the cards, hosts exactly ONE WebGL canvas for the ENTIRE section, never one canvas per card. Inside it: one `THREE.Scene`, one `THREE.PerspectiveCamera` (fov 34, positioned at `(0, 0, 10)` looking at the origin), one `THREE.WebGLRenderer({ alpha: true, antialias: true })` with `ACESFilmicToneMapping` (exposure 1.15 in dark theme, 1.0 in light theme), one `PMREMGenerator`-baked `RoomEnvironment` (blur 0.04) assigned to `scene.environment` for subtle reflections, one shared white `AmbientLight` (intensity 0.35), one shared white key `DirectionalLight` (intensity 2.0, positioned `(2.5, 3, 2.5)`), and exactly three gem groups in `tiers` order, each carrying its own small colored `PointLight` rim (decay 2, no distance cutoff) that flares in intensity while that card is active.
Every animation frame reads each card element's live `getBoundingClientRect()` (cheap, read-only) plus the overlay stage's own rect, and converts pixel offsets to Three.js world units at the z=0 plane with the perspective visible-height formula: `visibleHeight = 2 * cameraDistance * tan(fov_in_radians / 2)`, `worldUnitsPerPixel = visibleHeight / stagePixelHeight`. Each gem's anchor sits pinned at the card's top-right corner, offset 70px left of the card's right edge and 66px below the card's top edge, recomputed through that same px-to-world conversion every single frame so it tracks scroll, resize and the responsive card reflow with zero drift. This one-shared-canvas-above-everything approach is exactly why a grown gem can visually spill past its own card's edges even though the cards themselves use `overflow-hidden`: the gem never lives inside the clipped card element in the first place, it is drawn in an independent layer stacked above it. Never build one canvas per card, that reintroduces clipping and triples the GPU context cost.
Per-tier geometry (by array position, 1st/2nd/3rd): a four-sided cone flipped so its tip points down and its base is up (`ConeGeometry(1, 1.6, 4)`, scaled -1 on Y) for the first tier; a cube (`BoxGeometry(1.3, 1.3, 1.3)`) for the second; an octahedron (`OctahedronGeometry(1, 0)`, reading visually as a faceted diamond) for the third. Each solid is built from a translucent flat-shaded `MeshStandardMaterial` mesh (roughness 0.35, metalness 0, `side: DoubleSide`, `depthWrite: false`, emissive matching its face color at ~0.28 intensity) plus a separate `EdgesGeometry`/`LineBasicMaterial` wireframe overlay, plus a handful of small sphere "vertex dots" (radius 0.05) pinned exactly at that shape's geometric corners.
At rest (grow = 0) only a flat, in-plane (z=0) glyph is visible: a small emblem built from `LineSegments` plus sphere dots (radius 0.08), one glyph per tier: the pyramid tier's glyph is 3 dots (two upper corners, one bottom-center point) connected in a closed triangular path; the cube tier's glyph is 5 dots connected in a closed pentagon; the octahedron tier's glyph is 4 outer dots plus a center dot, connected as spokes radiating from the center (a small cross/asterisk read). As grow increases toward 1, the flat glyph's lines and dots fade out while the 3D solid's faces, edge wireframe and vertex dots fade in, so the glyph visually hands off into the solid's vertices rather than just being replaced.
Growth easing: a per-gem `grow` value lerps toward its target (1 while that card is active, else 0) at `grow += (target - grow) * min(1, dt * 8)` every frame; under `prefers-reduced-motion: reduce` it snaps straight to the target instead. Footprint size interpolates linearly from a 37.5px rest diameter to a 105px grown diameter (about 2.8x). The solid's tilt is `rotationX = 0.15 + 0.7 * grow` (it tilts further into a clear 3D read as it grows) and it only spins on Y while filling in, `rotationY += dt * 0.5 * grow`, so at full rest there is zero spin and zero bob, ever, and spin stops the instant grow returns to 0. Opacities scale with grow: solid face opacity `0.3 * grow` (stays translucent even fully grown, never opaque), wireframe edge opacity `0.7 * grow`, vertex-dot opacity `0.95 * grow`; the rest glyph's own dot and line opacities do the inverse, `0.95 * (1 - grow)` and `0.8 * (1 - grow)`. A soft radial contact shadow (one shared canvas-drawn gradient texture reused across all three gems) fades and tracks grow too, capped at `0.7 * grow` opacity in dark theme but only `0.22 * grow` in light theme, since the same fixed dark gradient would read far too harsh against a pale card.
Color logic: all three gems share exactly one accent palette at any moment (only the GEOMETRY differs by tier, not the hue). Dark theme: face/emissive/rim color `#FF8F3F`, edge/glyph-line color `#FFE6CC`, dot color `#FFFFFF`. Light theme: face/emissive/rim color `#B33716`, edge/glyph-line color `#8A4A32`, dot color `#3A221A` (deliberately darker and more saturated than the dark-theme palette, since the pale-cream dark-theme tones would nearly vanish against a light card). Detect the active theme by walking up to the nearest ancestor with a `data-card-theme="light"|"dark"` attribute, falling back to whether `document.documentElement` has the `dark` class, and only re-apply colors on the frame the detected theme actually changes.
INTERACTION MAP: on pointer-capable devices (`matchMedia('(pointer: coarse)')` false), `onPointerEnter`/`onPointerLeave` on each card element sets that card's index as the single "active" index (kept in both a ref, read inside the render-loop closure to avoid stale values, and React state, to drive the price-toggle re-render and touch UI); `onFocus`/`onBlur` on the card do the same for keyboard users, with the `onBlur` handler checking `relatedTarget` so tabbing between two focusable elements inside the SAME card never collapses it early. On touch devices, hover is disabled and a tap directly on the card (ignoring taps that land on its own `button`/`a` elements) toggles that card's active state instead; a `pointerdown` listener on `document` collapses whichever card is active the moment the user taps anywhere outside every card.
QUALITY BAR / TECH MANDATES: raw Three.js via dynamic `import('three')` and `import('three/addons/environments/RoomEnvironment.js')`, guarded with an "alive" flag so an unmount that happens while those imports are still resolving never touches a torn-down DOM; size the renderer and update the camera aspect from the stage element's own box via `ResizeObserver`; pause the render loop entirely when the section scrolls off-screen (`IntersectionObserver`, threshold 0.01) or the tab is hidden (`visibilitychange`), and resume cleanly on return; cap `devicePixelRatio` at `Math.min(window.devicePixelRatio, isTouch ? 1.5 : 2)`; on unmount, dispose every geometry, every material, the shared shadow texture, the shared shadow-plane geometry, the environment render target, the PMREM generator and the renderer itself, and remove the canvas node from the DOM. Respect `prefers-reduced-motion: reduce` everywhere motion is programmatic (grow snaps instantly, spin never accrues, the price crossfade animation is disabled via the CSS media query). Semantic markup throughout: the billing toggle is a real `button[role="switch"]` with `aria-checked`/`aria-label`, the checklist is a real `ul`/`li`, every clickable control has a visible `focus-visible` outline in the accent color, and the WebGL overlay is `aria-hidden` since it is decorative on top of fully accessible DOM controls. Font sizes only from the 12/14/20/32px set used above; spacing on a 4/8/12/16/20/24/32px-ish grid matching the exact values called out per element. Fully responsive from 320px (cards stack, Pro first) through desktop (three-up grid). No em-dashes anywhere in the component's copy.
Start the file with `// npm install three @phosphor-icons/react`. Default export the component as `GemPricingSection`.