From 658352b6940dff77524b1fecd5bbc50c7f77bc3d Mon Sep 17 00:00:00 2001 From: Matthew Meszaros Date: Wed, 27 May 2026 05:36:51 +0000 Subject: [PATCH] site: home pricing - swap price numbers with /pricing slide-up animation Cards stay edge-to-edge. Only the price-number swap changes. - Price markup adds the .plan-price > .plan-price-inner wrapper pair /pricing uses, with overflow-hidden on the parent so the new digit can rise from below the cropped box. - Toggle JS replaces the motion.dev opacity fade on .price-amount with the same two-phase translateY swap /pricing runs on .plan-price: phase 1 lifts and fades the old value to translateY(-100%), phase 2 drops the new value in from translateY(100%) with a cubic-bezier(0.34,1.56,0.64,1) overshoot. - Cadence line keeps its existing class but now does a soft 180ms opacity-out, text-swap, opacity-in instead of a hard snap, so it reads in sync with the price. - Thumb still animates with motion.dev animate(); the switcher behavior is unchanged. --- site/src/pages/index.astro | 54 ++++++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/site/src/pages/index.astro b/site/src/pages/index.astro index 2c83c0a0..3564e0f2 100644 --- a/site/src/pages/index.astro +++ b/site/src/pages/index.astro @@ -785,7 +785,11 @@ const plans = [

{p.summary}

- {p.annual} + + + {p.annual} + +

/ month · billed yearly @@ -1052,10 +1056,34 @@ const plans = [ }); // ── Pricing billing toggle (Monthly ↔ Annual, annual default) ─── + // Thumb still slides with motion.dev. Prices swap with the same + // slide-up rebound animation /pricing uses on its plan numbers. const toggle = document.getElementById('billing-toggle'); const thumb = document.getElementById('toggle-thumb'); if (toggle && thumb) { const btns = toggle.querySelectorAll('button[data-billing]'); + + // Slide-up swap on a single price number. Phase 1 lifts and fades + // the old value out; phase 2 drops the new value in from below with + // a subtle overshoot. + const animatePrice = (el, newValue) => { + const inner = el.querySelector('.plan-price-inner'); + if (!inner) return; + if (inner.textContent === newValue) return; + inner.style.transition = 'transform 220ms cubic-bezier(0.4,0,0.2,1), opacity 220ms cubic-bezier(0.4,0,0.2,1)'; + inner.style.transform = 'translateY(-100%)'; + inner.style.opacity = '0'; + setTimeout(() => { + inner.style.transition = 'none'; + inner.style.transform = 'translateY(100%)'; + inner.textContent = newValue; + inner.offsetHeight; // reflow + inner.style.transition = 'transform 320ms cubic-bezier(0.34,1.56,0.64,1), opacity 220ms ease-out'; + inner.style.transform = 'translateY(0)'; + inner.style.opacity = '1'; + }, 220); + }; + const apply = (mode, animated = true) => { const btn = toggle.querySelector(`button[data-billing="${mode}"]`); if (!btn) return; @@ -1076,17 +1104,27 @@ const plans = [ b.classList.remove('text-white'); } }); - document.querySelectorAll('.price-amount').forEach((el) => { + document.querySelectorAll('.plan-price').forEach((el) => { + const next = el.getAttribute(`data-${mode}`) || ''; + if (!next) return; + if (!animated) { + const inner = el.querySelector('.plan-price-inner'); + if (inner) inner.textContent = next; + return; + } + animatePrice(el, next); + }); + // Cadence text gets a soft fade-swap so it never just snaps. + document.querySelectorAll('.cadence').forEach((el) => { const next = el.getAttribute(`data-${mode}`) || ''; if (!next) return; if (!animated) { el.textContent = next; return; } - animate(el, { opacity: [1, 0] }, { duration: 0.12 }).then(() => { + el.style.transition = 'opacity 180ms ease-out'; + el.style.opacity = '0'; + setTimeout(() => { el.textContent = next; - animate(el, { opacity: [0, 1] }, { duration: 0.18 }); - }); - }); - document.querySelectorAll('.cadence').forEach((el) => { - el.textContent = el.getAttribute(`data-${mode}`) || ''; + el.style.opacity = '1'; + }, 180); }); }; btns.forEach((b) => b.addEventListener('click', () => apply(b.getAttribute('data-billing'))));