Price first. Visit later.
The design language for our heat-pump buy flow: nine steps, twenty named components and one visual system. Taken from Jetson's intake and Tesla's order pages, bent for HVAC.
The flow we adopt
One promise: your real price, before anyone visits. The address starts a price; every later step makes it more certain; the number never leaves the screen.
Jetson sells "1. See your price. 2. Upload a few photos. 3. Choose your install date" and asks only for an address up front. Tesla keeps the whole order on one page with a sticky price and takes a small refundable deposit. We take Jetson's low first ask and Tesla's always-visible price, and move the heavy questions (photos, panel, nameplate) after the deposit.
Jetson facts come from our own captures of jetsonhome.com on 2026-10-27 (phone 390 wide at 2x, desktop 1440). We stopped the intake at the address step so we never created a contact record. Tesla facts come from a live accessibility-tree read of tesla.com/modely/design on Jonathan's Mac (27 Oct 2026, cited as "Tesla live, AX read"; it outranks the rest where they disagree) and from secondary sources, since tesla.com is blocked from our server; each one is linked.
-
Address
- Change for HVAC
- Keep the single field and the three promises, but reword the third to the real promise: "Get a real price, not a visit." Pre-fill the utility from the address the way Tesla Energy does, so the rebate step already knows the programs. Create the record only after step two, not on suggestion pick.
- Ask for
- Street address (Places suggestion). Nothing else.
- Button
Where this comes from
Jetson's intake: the whole first step is one Google Places combobox under three check-marked promises ("Find your rebates and incentives / See a system tailored for your home / Get your free, upfront price"), with city, state and ZIP locked until a suggestion is picked and an "Edit full address details" expander. Picking a suggestion posts to the server at once. Source: jetsonhome.com/intake.
Jetson /intake, phone, empty state. jetsonhome.com/intake -
Home details
- Change for HVAC
- A heat pump needs a load, so we ask the six sizing facts here and keep photos for after the deposit. The "existing equipment" question becomes "What heats your home today: gas furnace / oil / electric baseboard / heat pump / none".
- Ask for
- Square feet, year built, heating fuel today, ducts yes/no, floors and zones, average monthly energy bill, existing AC yes/no. Photos (nameplate, panel, ductwork) come later as tasks.
- Button
Where this comes from
Tesla Energy asks almost nothing before the order: address, average monthly bill, product, and on the Powerwall path "Existing equipment: Solar / Powerwall / None". Bill upload, panel photos and the site survey are tasks after the deposit. Source: tesla.com how-to-order-solar-panels (Wayback). Jetson's step two is "Upload a few photos".
Jetson homepage, "Everything handled" step cards. jetsonhome.com/us/air -
Sizing recommendation
- Change for HVAC
- The recommendation is a Manual J-lite result stated in tons and BTU with the one-line reason ("2.5 tons for 1,800 sq ft in Denver, 1998, gas furnace, ducted"). The second card is "Plan for an addition, EV charger or ADU". No "Customize" slider: oversizing is the thing we are selling against.
- Ask for
- Future loads (addition, EV, ADU, finished basement). Optional.
- Button
Where this comes from
Tesla Energy's "Recommended" card against a "Plan for Future Use" card, each with its own $/mo, plus an "Edit" for future loads ("1 new EV") and a "Customize" row. Source: Electrek, 2025-10-03. Jetson's claim "Not oversized. Just the right size." (jetsonhome.com/us/air).
Tesla Energy design rail, Lease tab selected. electrek.co -
Options and add-ons
- Change for HVAC
- Three equipment tiers (Good, Better, Best) named by what changes: cold-climate rating, SEER2, sound, warranty years. Add-ons as a checklist with a price each: air purification, smart thermostat, humidity control, maintenance plan, panel upgrade if the sizing step flagged it.
- Ask for
- One tier. Any add-ons.
- Button
Where this comes from
Tesla's trim cards: each card carries its own price, or its own $/mo once a finance tab is selected, so the buyer compares deltas without arithmetic; accessories below are line items with an Add button. Source: r/TeslaModelY 1g881qs. Tesla live, AX read, 27 Oct 2026: add-ons are priced checkboxes in the rail (Wall Connector $585 "Installation required", Tow Hitch $1,000, accessories $30 to $500) and Full Self-Driving is a $99/mo subscription checkbox. Jetson's add-on is Jetson Care Plus (jetsonhome.com/us/care-plus).
Model Y trim cards on the Finance tab. reddit.com r/TeslaModelY -
Rebates and incentives
- Change for HVAC
- Three named lines: federal 25C credit, HEEHRA rebate (income-qualified, marked "if eligible"), utility rebate from the pre-filled utility. Two totals, always both: the real price and "after incentives". Energy-bill savings are a separate opt-in checkbox and never mix into the payment figure (Tesla's "after est. savings" drew the complaints). We do not pre-check an income-qualified rebate the way Tesla pre-checks the state incentive: HEEHRA is unchecked until the buyer confirms the criteria.
- Ask for
- Household size and income band for HEEHRA, only if the buyer wants that line checked. Optional.
- Button
Where this comes from
Tesla Energy nets incentives as negative line items with their program name ("Solar & Powerwall Discount −$4,900") above a "Cash Price, taxes and fees not included". On the Model Y the incentive is a checkbox: Tesla live, AX read, 27 Oct 2026 shows a pre-checked "$3,500 Massachusetts Incentive" with a "View Criteria" link, netted into the monthly the bar shows ("$526 /mo after est. savings"), plus a separate "Up to $6,600 potential savings after delivery" button. The 2024 configurator had the same checkbox with gas savings folded in. Sources: r/TeslaSolar 1nuxczq, Teslarati, 2023-02;.
Tesla Energy breakdown and deposit copy. electrek.co -
Payment
- Change for HVAC
- Three segments: Cash, Finance, Lease-or-subscribe. Finance shows term and APR band and a down-payment slider; the monthly estimate changes as the slider moves. Lease captures the incentives for the owner and includes maintenance, the way Tesla's lease "builds in" the federal credit. No credit-tier question; a "Get prequalified" link is allowed as Tesla's side door (optional, soft pull, never a gate). The default segment is Finance when a promotional rate exists, otherwise Cash. The demo at the foot of this page is this step, working.
- Ask for
- Payment mode. For finance: down payment and term. Nothing about credit.
- Button
- (the label repeats the chosen mode)
Where this comes from
Tesla's Cash / Lease / Finance tabs at the top of the rail re-price every card at once; the monthly figure carries its assumptions inline ("$600 down, 3% annual escalator, 25 years") with an "Edit terms" link; "Compare Payment Types" opens a three-column sheet; APR is shown, never entered, and the credit application comes after the deposit. Tesla live, AX read, 27 Oct 2026: the tab group is Cash | Lease | Finance with Finance selected by default, every trim card priced monthly ($526/mo RWD), the terms line "2.49% APR, $2,750 down, 72 mo" with "Edit Financing", and two side-door buttons under the cards, "Get Trade-In Value" and "Get Prequalified". Secondary sources: r/TeslaSolar 1nuxczq, Model Y walkthrough, 2026-01;.
Tesla Energy "Compare Payment Types". reddit.com r/TeslaSolar -
Install date
- Change for HVAC
- Jetson's placement wins: the buyer picks a preferred install week before the deposit, which is the moment the whole thing feels real. The week is held, then confirmed after the site visit; the copy says so under the picker.
- Ask for
- A preferred week and a second choice. Morning or afternoon.
- Button
Where this comes from
Jetson's third promise, "3. Choose your install date", sits on the homepage next to "Switch in a day. One team. One price." (jetsonhome.com/us/how-it-works). Tesla schedules delivery after the order, as an app task ("Schedule delivery", walkthrough, 2025-05).
Jetson's closing address card, phone. jetsonhome.com/us/air -
Order summary and deposit
- Change for HVAC
- "Due today: $100, refundable until you approve the final design after the home visit. Your price can change if the visit finds something the photos did not: panel capacity, duct condition, line-set routing." Itemize every fee before the button (permit, disposal of the old unit, electrical). Going back is always allowed; "Approve design" gets a confirm dialog. The address from step one stays at the top of the rail as an editable chip, the way Tesla keeps the delivery location there.
- Ask for
- Nothing new. A read-back of every choice with an edit link per line.
- Button
Where this comes from
Tesla's summary is the bottom of the same rail, not a new page: itemized options, "Est. Purchase Price, includes destination and order fee", the payment line with its terms, "Sales Tax: Pending", then a bold Due Today. Energy's wording under it: "$100. Refundable deposit until you accept your design. Price is subject to change based on installation complexity." Tesla live, AX read, 27 Oct 2026: the sticky bottom bar carries the configuration summary ("Model Y · Rear-Wheel Drive · Stealth Grey · 18'' Aperture Wheels · All Black"), a "$526 /mo after est. savings" button that opens the breakdown, and the primary "Order Now"; the delivery location ("Boston, MA, 02125") is an editable chip at the top of the rail. Sources: The Weekly Driver, 2026-07, Electrek, 2025-10; (left panel).
Model Y summary: fees itemized, Due Today, Apple Pay then card. reddit.com r/TeslaModelY -
Account, payment, then tasks
- Change for HVAC
- Account is created by the deposit, not before it. The task list after: photos of the nameplate, panel and ducts; the home visit; the financing application (only now); confirm the held week; final payment on install day. Each task shows what it unlocks.
- Ask for
- Name, email, phone, card or wallet, one agreement checkbox.
- Button
Where this comes from
Tesla: Apple Pay (black) or "Order with Card", then name, email, phone, card and one agreement checkbox, then "Place Order". Everything heavy moves to a numbered task list in the app ("Step 7 of 8, Final Payment"): registration, financing application, insurance, trade-in photos, delivery date, final payment. Sources: Model Y walkthrough, 2026-01, motorandwheels.com; our Tesla notes, Model Y steps 14 to 16.
Tesla's inline trade-in credit, the model for "Get credit for your old system" as a later add. driveteslacanada.ca
Pattern catalogue
Every pattern in the flow maps to one named component. Order of search, per design-standard §6.1: React Bits, then 21st.dev, then a GSAP plugin; hand-build only with a written reason.
Install from the author's own registry where one exists (Origin UI, Motion Primitives, React Bits). Installing through 21st.dev itself needs a 21st API key we do not have; the free plan allows two copies a day. Every component is restyled to the one ink, the one accent, Oswald and the 8px grid before it ships (§6.1 rule 4), and sits behind one reduced-motion guard (§6.1 rule 3). Licences were read from each component page on 2026-10-27; entries marked check licence list "unknown" or "no-license" on 21st and are layout references only until the author confirms.
-
Address combobox
Step one. Jetson's Places field.
- Component
- Combobox by Abraham (Ark UI) on 21st.dev
- Licence
- Ark UI is MIT at source; the 21st listing says unknown. check licence on the demo wrapper only.
- Install
bun add @ark-ui/react, then the Combobox from ark-ui.com. Fallback:bunx shadcn@latest add "https://21st.dev/r/anubra266/combobox?api_key=$API_KEY_21ST"- Change
- One field, Places session token, suggestions listed as street then city in
--ink-2, the "Edit full address details" row as the Accordion below. The arrow button is the accent, 48px square.
-
Vertical progress rail
Every step. Jetson's five dots, this page's spine.
- Component
- Stepper, React Bits (vertical layout, gliding indicator). Static or non-React pages: Stepper with inline titles and descriptions, Origin UI.
- Licence
- React Bits: MIT + Commons Clause, free for our sites. Origin UI: MIT.
- Install
- Origin UI from originui.com/stepper
- Change
- Dots in the ink on a 1px hairline, no spring: Hint level means the indicator moves with a 200ms ease-out, and under reduced motion it jumps. Step names in Oswald caps labels.
-
Payment segmented control
Step six. Tesla's Cash | Lease | Finance tab group, Finance default (Tesla live, AX read, 27 Oct 2026).
- Component
- Animated Tabs, Motion Primitives (Julien Thibeaut), the Animated Background highlight the design standard names for Hint. Alternative without
motion: Segmented Control, halaska-studio. - Licence
- Both MIT.
- Install
bunx shadcn@latest add https://motion-primitives.com/c/animated-background.json- Change
- Track in
--ground-3, selected segment the accent fill with ground text, square corners, labels in Oswald caps. Switching re-prices every card on the screen at once (Tesla rule).
-
Down-payment slider
Step six. Tesla's "Edit terms".
- Component
- Slider, Origin UI (Radix slider with a value output). Not React Bits Elastic Slider: its rubber-band easing is on the §6.5 list.
- Licence
- MIT.
- Install
- From originui.com/slider (shadcn registry JSON per variant).
- Change
- Track in
--ground-3, filled range and thumb in the ink, the live value in Oswald tabular figures beside it. Term is a plain select, not a slider.
-
Live price figure
Steps three to eight. Tesla's instantly updating price.
- Component
- Number Flow, SmoothUI (Edu Calvo), rolling digits on the
number-flowpackage. React Bits Count Up is excluded: §6.5 bans counting on numbers that do not need it, and a price that changes needs a roll, not a count from zero. - Licence
- MIT.
- Install
bun add @number-flow/react, wrapper from smoothui.dev- Change
- Oswald, tabular numerals, no blur on the digits under reduced motion. Only the figure rolls; the terms line beneath it just changes.
-
Sticky price bar
Steps three to eight. Tesla's bottom bar: summary, price button, Order Now (Tesla live, AX read, 27 Oct 2026).
- Component
- Bottom Nav Bar, Arunachalam as the fixed shell, with Number Flow inside and the Accordion below for "Show pricing details". check licence (listed unknown); the shell is a 40-line
position: stickyblock if it fails. - Licence
- Unknown on 21st.
- Install
bunx shadcn@latest add "https://21st.dev/r/arunachalam/bottom-nav-bar?api_key=$API_KEY_21ST"- Change
- Ground fill, one hairline on top, no shadow, no blur. Left: figure and one terms line. Right: the one accent button. Never animated; it is the CTA (our motion rules).
-
Option cards (radio cards)
Steps three and four. Tesla's trim and package cards.
- Component
- Radio Group with grid, Origin UI. Richer alternative: Radio Group with Plan Cards, ShadcnSpace check licence.
- Licence
- Origin UI MIT.
- Install
- From originui.com/radio
- Change
- Selected state is a
--ground-3wash plus a check glyph, never an accent outline (frontend draft guard). Each card shows its own $/mo in the selected payment mode.
-
Add-on checklist
Step four. Tesla's accessories list.
- Component
- Checkbox, Origin UI in a list row with the price right-aligned.
- Licence
- MIT.
- Install
- From originui.com/checkbox
- Change
- 24px box, accent when checked, tabular price. The same checkbox is the incentive opt-in in step five (Tesla's "$3,500 Massachusetts Incentive · View Criteria", Tesla live, AX read, 27 Oct 2026) and the energy-savings opt-in.
-
Price lines and summary
Steps five and eight. Tesla's negative lines and "Sales Tax: Pending".
- Component
- Checkout Summary, astralyxdev: subtotal, discount, tax and shipping lines, with unknown charges shown as "calculated at next step" instead of a false zero.
- Licence
- MIT.
- Install
bunx shadcn@latest add "https://21st.dev/r/astralyxdev/checkout-summary?api_key=$API_KEY_21ST"(no author registry; 2 free copies a day)- Change
- Rebate lines carry the program name and a negative figure in the accent; two totals, "Price" and "After incentives"; "Sales tax: pending until the home visit".
-
Compare payments sheet
Step six. Tesla's three-column compare.
- Component
- Sheet, Sean Hello (bottom sheet on phone, side panel on desktop) check licence; inside it, three columns of the pane shown in the demo below.
- Licence
- Not stated on 21st; fallback is shadcn/ui Sheet (MIT).
- Install
bunx shadcn@latest add sheet- Change
- Ground fill, no blur backdrop (a plain 40% ink scrim), square corners, a close control top right and Escape to close.
-
Accordion
FAQ, "Show pricing details", "Edit full address". Steps one, five, eight. Jetson's expander, Tesla's chevron.
- Component
- Accordion, Origin UI
- Licence
- MIT.
- Install
- From originui.com/accordion
- Change
- Hairline between items, a plus that turns to a minus in the ink, no card frames. Height animates 200ms ease-out; under reduced motion it snaps.
-
Install week picker
Step seven. Jetson's "Choose your install date".
- Component
- Calendar [React Day Picker], date-picker variant, Origin UI
- Licence
- MIT.
- Install
- From originui.com/calendar
- Change
- Weeks, not days: a week row is one target, unavailable weeks dimmed. The held week shows as an accent fill, the second choice as a
--ground-3wash.
-
Trust strip
Step one. Jetson's "Trusted by 2,500+ homes, 4.9 stars" and the Google review.
- Component
- Testimonial, avatar group with trust label, PrebuiltUI check licence (layout reference; the strip is three spans and one SVG star).
- Licence
- Not stated on 21st.
- Install
- Hand-built from the layout: count, rating, one short quote, in Small, under the address card.
- Change
- No avatars, no logo wall. Ink text, one star glyph drawn as SVG, never the accent.
-
Number fields
Step two. Square feet, floors, zones.
- Component
- Number Field, coss (Base UI number field with step buttons).
- Licence
- MIT (coss.com components).
- Install
bunx shadcn@latest add https://coss.com/r/number-field.json- Change
- Square corners, 48px tall,
inputmode="numeric", unit in--ink-2inside the field. Fuel, ducts and existing AC are radio cards, not selects.
-
Photo upload
Step nine tasks. Jetson's "Upload a few photos".
- Component
- File Upload, multi-file dropzone, Ephraim Duncan check licence.
- Licence
- Not stated on 21st.
- Install
bunx shadcn@latest add "https://21st.dev/r/ephraimduncan/file-upload-1?api_key=$API_KEY_21ST"- Change
- Three named slots (nameplate, panel, ducts) each with an example line drawing in the ink, camera capture on phone. No dashed border: a
--ground-2field.
-
Express pay and card
Step nine. Tesla's Apple Pay then "Order with Card".
- Component
- Express Wallets, felipemenezes098: Apple Pay and Google Pay buttons with a card fallback. Listed no-license, so layout reference only; the real control is Stripe's Payment Element.
- Licence
- No licence on 21st. Do not copy its code.
- Install
bun add @stripe/stripe-js @stripe/react-stripe-js, Payment Element with wallets on- Change
- Wallet button first in the ink, then "Pay with card" as the secondary button, then the one agreement checkbox and the deposit copy from step eight.
-
Post-order task list
Step nine. Tesla's "Step 7 of 8" delivery tasks.
- Component
- The same Origin UI Stepper vertical, one row per task with its state. Reference for the panel-and-progress layout: Onboarding Checklist, Caio Bonato check licence.
- Licence
- Origin UI MIT.
- Install
- As the rail above.
- Change
- Each row names what it unlocks ("Photos: lets us confirm your price"). Done rows get a check glyph in the ink; the next one is the only accent on the screen.
-
Bill savings chart
Step five, opt-in. Tesla's with-and-without bars.
- Component
- Bar Chart, Intent UI (Recharts). Two bars: this year's bill with the old system, with the heat pump.
- Licence
- MIT (Intent UI).
- Install
bunx shadcn@latest add https://intentui.com/r/bar-chart.json- Change
- Two bars in
--ground-3and the ink, figures in Oswald above them, assumptions in a Small footnote (rate, degree days, fuel price), no grid lines, no tooltip.
-
Section rise on scroll
Every step below the first screen. The Hint-level quiet rise.
- Component
- Fade Content, React Bits (plain fade, not grain or mist). Static pages, as here: GSAP ScrollTrigger through the shared motion setup.
- Licence
- MIT + Commons Clause. GSAP free since 3.13.
- Install
bunx shadcn@latest add @react-bits/FadeContent-TS-TW- Change
- 24px rise, 0.6s, power3.out, once, never on anything in the first viewport and never on the CTA. One guard:
gsap.matchMedia()onprefers-reduced-motion: no-preference.
-
Trade-in credit sheet
A future add to step four. Tesla's inline "Get Trade-In Value".
- Component
- The Sheet above, with the Number Field for the old unit's age and the live figure. Pattern source: Drive Tesla Canada, 2025-11.
- Licence
- As above.
- Install
- As above.
- Change
- "Credit for your old system" with the footnotes Tesla uses: valid 7 days, subject to inspection. After Continue, every price drops by the credit and the bar says "Includes est. credit".
Visual language
One ground, one ink, one accent. One display family. An 8px grid. Motion with a job. A seal and a hairline for the Greek, and nothing classical near a button.
This is the Hint level of Jonathan’s design standard, chosen because the flow is a sales funnel (§2.3): the action wins, so the type does the Greek work and the only classical marks are the seal in the header and footer and the one hairline with dot terminals under the hero.
Colour
The accent appears on the primary button, on links and on the selected payment segment. Never as a fill behind text, never as a card outline, never past a tenth of a screen. Surfaces step in tone; nothing is outlined or lifted.
Spacing
- 4
- 8
- 16
- 24 · phone gutter
- 40
- 64
- 96 · between sections, phone
- 160 · between sections, desktop
Type
Price first.
Visit later.
Lead in the system sans at 21 → 26px.
Body at 17 → 19px, line height 1.55, at most 62 characters a line. Figures in Oswald with tabular numerals: $177 /mo
Motion, each with its job
- Carry the story on scroll: Lenis smooth scroll and one 24px rise per section below the first screen. That is the whole Hint allowance.
- Answer the hand: the segment control and every price re-price at once; the figure rolls, nothing else moves.
- Never: the sticky bar and its button, anything in the first viewport, text effects above the fold. Reduced motion renders everything in place; the page works with scripts off.
The Greek, at Hint
One seal: circle, crosshair, a laurel sprig, 32 to 48px in the ink, as the wordmark and the footer mark. One hairline with dot terminals as the divider after the hero. Construction lines are 1px at 45 to 60% ink and never carry text. No statue, no etching: at Hint the type is the Greek.
Motion system
What Jetson's pages do when you scroll and click, and what we build the same moments with. Four engines of theirs, one toolkit of ours, 38 patterns matched one to one.
Jetson's engines
- Scroll timeline
- Scroll progress drives a table of phases per element in every pinned scene.
- Snap engine
- Wheel, keys and swipes are captured and glided to scene steps.
- Observer reveals
- Entering the viewport adds a class; Tailwind transitions do the rest.
- CSS keyframes
- Menus, accordions, halos, modals and spinners.
Our toolkit
- GSAP + ScrollTrigger
- Every scrubbed scene, pin and tween; one engine per page.
- Lenis
- Native-feeling smooth scroll, never a captured wheel.
- React Bits
- Reveal and text pieces, restyled to the one ink.
- 21st.dev registries
- Tabs, accordions, menus and the other structural controls.
Where this comes from
Jetson's scroll timeline reads progress through framer-motion's scroll(); framer-motion is bundled but only its runtime is used. There is no GSAP, no ScrollTrigger, no Lenis and no Lottie on the site. The engines and tokens were read from the shipped bundles and stylesheets, then checked against our screen recordings of the home, Air and About pages.
Each of the 38 patterns below names the component we would reach for, with its link, and shows the literal GSAP config where a recipe is the right answer. Everything sits behind one reduced-motion guard, and nothing in a first viewport starts hidden.
Tokens
Jetson keeps six durations and seven curves in one token module and reuses them everywhere. We adopt the three durations and the two curves we need; the rest are listed so a recording can be read against them.
| Token | Jetson | Ours (GSAP seconds) | Jetson uses it for |
|---|---|---|---|
| fast | 200ms | 0.2 | Statement text per grapheme; the dialog scrim; menu items leaving. |
| base | 300ms | 0.3 | Accordion, tabs, nav underline, dialog, header narrowing, row steps. |
| slow | 600ms | 0.6 | Section rise, feature stagger, header hide, menu items arriving, the hero load-in. |
| glide-short | 460ms | not used (no snap) | The shortest snap glide. |
| glide-long | 760ms | 0.76 for the long reveals | The longest snap glide; the card stagger; the track wipe. |
| held-1 | 1250ms | not used (nothing takes more than a second) | The phone menu sweep; the intro overlay lift. |
| Token | Jetson | Ours (GSAP ease) | Jetson uses it for |
|---|---|---|---|
| reveal | cubic-bezier(0.22, 1, 0.36, 1) | expo.out, or exact: CustomEase.create("reveal", "0.22,1,0.36,1") | Accordion answers, the nav underline, the mega-menu clip, menu items, scene phases on desktop. |
| emphasis | cubic-bezier(0.5, 0, 0.1, 1) | power3.inOut; only for a scene change | The phone menu sweep. |
| standard | cubic-bezier(0.2, 0.7, 0.2, 1) | power3.out (our default ease-out) | Section rise, statement text, the hero load-in, the header hide, the accordion icon, the scrim. |
| move | cubic-bezier(0.65, 0, 0.25, 1) | power2.inOut; only for a scene change | The intro overlay lift. |
| glide | cubic-bezier(0.65, 0, 0.35, 1) | not used (no snap) | Snap glides, the phone-side scene phases, the track wipe. |
| scrub | cubic-bezier(0.3333, 0, 0.6667, 1) | none: scrubbed scenes track the scrollbar | Whole-scene progress on the Air scenes. |
| inout | cubic-bezier(0.645, 0.045, 0.355, 1) | power2.inOut | The careers frame. |
- revealcubic-bezier(0.22, 1, 0.36, 1)
- emphasiscubic-bezier(0.5, 0, 0.1, 1)
- standardcubic-bezier(0.2, 0.7, 0.2, 1)
- movecubic-bezier(0.65, 0, 0.25, 1)
- scrubcubic-bezier(0.3333, 0, 0.6667, 1)
- inoutcubic-bezier(0.645, 0.045, 0.355, 1)
Each dot crosses its track in 600ms on the curve named beside it. Under reduced motion the dots change place without travelling.
Scroll scenes
Pinned sections whose elements move with the scrollbar. Jetson has eighteen; ours become one ScrollTrigger timeline each, scrubbed 1:1, with Lenis underneath and never a captured wheel.
Scroll-timeline engine
Every pinned scene on the home, Air, About, Water and Charge pages.
- Triggerscroll, pinned
- Durationscrubbed 1:1
- Easingper-phase tokens
- Movesticky at 100svh
OursScrollTrigger, GSAP
Details and recipe
- Trigger
- An IntersectionObserver wakes the scene; while it is on screen, scroll progress (0 to 1 over the wrapper's travel) drives a declarative table of phases per element.
- Values
- Scrub 1:1, no smoothing · pin: position sticky at 100svh inside a tall wrapper · per-phase curves from the easing tokens below · reduced motion renders the end state once.
- Ours
- ScrollTrigger, GSAP. One ScrollTrigger timeline per scene replaces the engine. Jetson pins with sticky; we let ScrollTrigger pin.
gsap.timeline({ scrollTrigger: { trigger: ".scene", start: "top top", end: "+=2700", pin: true, scrub: true, invalidateOnRefresh: true } })
Scroll snap to scene steps
Pinned scenes with steps on the home, Air, About and How it works pages, desktop only (over 640px).
- Triggerwheel, keys, swipe
- Duration460–760ms
- Easingglide
- Moveto the next step
OursScrollTrigger, GSAP Not adopted.
Details and recipe
- Trigger
- Wheel, arrow and page keys and swipes are captured while a snap region is in view and glided to the next step.
- Values
- Glide min(760, 460 + 0.3 × distance) ms · cubic-bezier(0.65, 0, 0.35, 1) · off under reduced motion.
- Ours
- ScrollTrigger, GSAP. Not adopted. Our motion rules forbid scroll-jacking and Lenis stays native. When a scene needs steps, ScrollTrigger snaps after the hand stops and never takes the wheel:
snap: { snapTo: [0.35, 0.68, 1], duration: { min: 0.46, max: 0.76 }, ease: "power2.inOut" }
Hero video clips into a window
Home, “Your home. Upgraded.”
- Triggerscroll
- Duration30vh of travel
- Easinglinear
- Moveinset 24 / 308px
OursScrollTrigger, GSAP
Details and recipe
- Trigger
- The first 30vh of scroll; the hero is sticky at 100vh inside a 130vh wrapper.
- Values
- Clip-path inset 0 → 24px sides and 308px bottom, 20px radius · title moves 24px right and 308px up, copy mirrors it · press bar fades in · linear with scroll (about 270px at 900px tall).
- Ours
- ScrollTrigger, GSAP. Recipe. In our flow the headline stays put and the action never moves; only the window clips.
gsap.timeline({ scrollTrigger: { trigger: ".hero", start: "top top", end: "+=30%", scrub: true } }) .to(".window", { clipPath: "inset(24px 24px 308px 24px round 20px)", ease: "none" }, 0)
Products row travels sideways
Home, the three product cards under “Jetson brings heating, hot water and EV charging together”.
- Triggerscroll, pinned, snap
- Duration1,600px of travel
- Easinglinear
- Moverow about 840px
OursScrollTrigger, GSAP
Details and recipe
- Trigger
- A 250vh wrapper with a sticky row; the row moves over the last three quarters of the travel.
- Values
- Row x 0 → centred on the last card (about 840px at 1440) over about 1,600px of scroll · linear · snap steps at 0.25, 0.625 and 1 · the whole row scaled to fit the viewport height.
- Ours
- ScrollTrigger, GSAP. ScrollTrigger recipe with snap after the hand stops:
gsap.to(".row", { x: () => innerWidth / 2 - lastCardCentre, ease: "none", scrollTrigger: { trigger: ".products", start: "top top", end: "bottom bottom", scrub: true, snap: [0.25, 0.625, 1] } })
Title lifts while the hub slides under it
Home, “The brains.”
- Triggerscroll, pinned
- Duration1,156px of travel
- Easinglinear
- Movetitle −450px
OursScroll Float, React Bits
Details and recipe
- Trigger
- Pinned; the lift runs over the first 78% of the travel.
- Values
- Title y 0 → −450px over about 1,156px of scroll · linear.
- Ours
- Scroll Float, React Bits. Closest named piece is React Bits Scroll Float; plain GSAP is simpler:
gsap.to(".title", { y: -450, ease: "none", scrollTrigger: { trigger: ".hub", start: "top top", end: "78% bottom", scrub: true } })
Collage slides in, centre window changes
Home, “The next generation of your home.”
- Triggerscroll
- Duration80vh of travel
- Easingsmoothstep
- Movetiles 320px
OursScrollTrigger, GSAP
Details and recipe
- Trigger
- Not pinned: progress runs from the section top being 80vh below the viewport top until it reaches the top.
- Values
- Side columns translate in from off-grid to 0 in the second 40% of progress, top-left and bottom-right tiles 320px vertically · centre window clip widens, photo pans · smoothstep.
- Ours
- ScrollTrigger, GSAP. Recipe:
gsap.timeline({ scrollTrigger: { trigger: ".next-gen", start: "top 80%", end: "top top", scrub: true } }) .fromTo(".window", { clipPath: "inset(0 round 13px)" }, { clipPath: "inset(0 36% 0 23% round 13px)", ease: "power1.inOut", duration: .6 }, 0)
App card expands to near full screen
Home, “Your home, in your hands.”
- Triggerscroll
- Duration80vh of travel
- Easingsmoothstep
- Moveinset 32px
OursScroll Expand, React Bits
Details and recipe
- Trigger
- Not pinned; smoothstep over 80vh of scroll as the section arrives.
- Values
- Clip from edge-to-edge to inset 32px with a 16px radius; height 90vh → viewport minus 64px · photo scale 0.94 → 0.96 · labels track the edges.
- Ours
- Scroll Expand, React Bits. Config: startWidth 99, startHeight 95, endRadius 16, mediaZoom 1.04, scrollDistance 0.8, smoothing 0 (Jetson goes edge-to-inset, the inverse of the usual expand).
Quote card widens from the centre column
Home, “Get your custom price fast.”
- Triggerscroll, pinned
- Duration120vh of travel
- Easinglinear
- Moveinset 356px → 0
OursScrollTrigger, GSAP
Details and recipe
- Trigger
- Sticky 100vh inside a 220vh wrapper; progress over 120vh.
- Values
- Card clip inset 0 356px → 0 with a 16px radius · text and actions fade in and settle · linear.
- Ours
- ScrollTrigger, GSAP. Recipe. Adopt the card; in our flow the quote action is visible from the start and never fades.
gsap.timeline({ scrollTrigger: { trigger: ".quote", start: "top top", end: "bottom bottom", scrub: true } }) .fromTo(".card", { clipPath: "inset(0px 356px round 16px)" }, { clipPath: "inset(0px 0px round 16px)", ease: "none" }, 0)
Install window grows to the full frame
Jetson Air, “Jetson Air installation”, after “We do it all.”
- Triggerscroll, pinned
- Duration2,700px of travel
- Easingease-in-out cubic
- Movewindow 600px → full
OursScroll Expand, React Bits
Details and recipe
- Trigger
- Pinned; a 3,600px wrapper gives 2,700px of travel at 1440×900.
- Values
- Window scaleX 600px → full width over 4–30% of the travel (about 700px of scroll), ease-in-out cubic · dim fades in · intro words split apart · description rises · final copy linear.
- Ours
- Scroll Expand, React Bits. Config: startWidth 42, startHeight 100, startRadius 16, endRadius 16, scrollDistance 3, holdDistance 0.35, overlayScrim 0.5, smoothing 0.
App window grows to full bleed
Jetson Air, “Your whole home. In your hand.”
- Triggerscroll, pinned
- Duration1,160px of travel
- Easingscrub token
- Movewindow 346 → 1,376px
OursScroll Expand, React Bits
Details and recipe
- Trigger
- Pinned; 2,060px wrapper, 1,160px of travel.
- Values
- Window 346px → 1,376px wide · headline and caption scale from a 1,728px artboard and slide from the window corner · cubic-bezier(0.3333, 0, 0.6667, 1) over the whole scene, photo on smoothstep.
- Ours
- Scroll Expand, React Bits. Config: startWidth 24, startHeight 58, scrollDistance 1.3, plus one GSAP tween for the headline scale.
Photo shrinks into a framed card, columns slide in
Jetson Air, “Not oversized. Just the right size.”
- Triggerscroll, pinned, snap
- Duration3,525px of travel
- Easingscrub token
- Moveclip 27.5% sides
OursScrollTrigger, GSAP
Details and recipe
- Trigger
- Pinned; 4,425px wrapper (3,525px of travel) with snap steps.
- Values
- Clip-path inset 0 → about 5% top and bottom, 27.5% sides, 16px radius, over the first 28% (about 995px) · then left and right columns from x ±32px and hidden to in place · cubic-bezier(0.3333, 0, 0.6667, 1).
- Ours
- ScrollTrigger, GSAP. Recipe; the snap settles after the hand stops:
scrollTrigger: { trigger: ".sizing", start: "top top", end: "bottom bottom", scrub: true, snap: { snapTo: [0.3529, 0.6765, 1], duration: { min: 0.46, max: 0.76 }, ease: "power2.inOut" } }
Window scales up as two words split (phone only)
Jetson Air, “Cleaner air, built in.”
- Triggerscroll, pinned (phone)
- Durationscrubbed
- Easingease-in-out cubic
- Movewords ± half the window
OursScrollTrigger, GSAP
Details and recipe
- Trigger
- Pinned; rows exist only under 640px, so the desktop scene is static.
- Values
- Window scale 0 → 1 over 6–50%, ease-in-out cubic · the two words move apart by half the window plus 16px and fade at 32–42% · copy appears at 60%.
- Ours
- ScrollTrigger, GSAP. ScrollTrigger inside gsap.matchMedia:
mm.add("(max-width: 639px)", () => tl.fromTo(".window", { scale: 0 }, { scale: 1, ease: "power2.inOut" }, 0.06) .to(".wordL", { y: -(h / 2 + 16), autoAlpha: 0 }, 0.32) .to(".wordR", { y: h / 2 + 16, autoAlpha: 0 }, 0.32))
Photo card opens from a small window
Jetson Water “4x more efficient by design”; Jetson Charge “Charging that looks after itself.” and “One system. Way more than charging.”
- Triggerscroll, pinned
- Durationscrubbed
- Easingreveal · glide on phone
- Moveclip opens to the card
OursScrollTrigger, GSAP
Details and recipe
- Trigger
- The wrapper overlaps the previous section by 62vh on desktop (100vh on phone) and opens while it scrolls into place.
- Values
- Clip inset from a centred window to the full card, 16px radius · ease-out cubic on desktop, in-out on phone · copy reveals in 600ms after 400ms.
- Ours
- ScrollTrigger, GSAP. Recipe:
gsap.timeline({ scrollTrigger: { trigger: ".scroll-window", start: "top 62%", end: "top top", scrub: true } }) .fromTo(".mask", { clipPath: "inset(0% 35% 70% 35% round 16px)" }, { clipPath: "inset(0% round 16px)", ease: "power3.out" })
Reserve call to action: window opens, a wash fades in
Jetson Water and Charge, “Reserve your Jetson … today.”
- Triggerscroll, pinned
- Durationscrubbed
- Easingease-out cubic
- Movewash 0 → 1
OursScrollTrigger, GSAP
Details and recipe
- Trigger
- Overlaps 100vh at both ends; progress over the pin range.
- Values
- Mask inset (1 − f) × 50% sides and (1 − f) × 100% bottom → 0, f = min(1, 0.3 × viewport width ÷ card width), ease-out cubic in 16 steps · colour wash opacity 0 → 1 · copy at 97%.
- Ours
- ScrollTrigger, GSAP. Recipe. Our wash is a tone step of the ground, never a second colour.
.fromTo(".mask", { clipPath: "inset(0% 35% 70% 35% round 16px)" }, { clipPath: "inset(0% round 16px)", ease: "power3.out" }, 0) .fromTo(".wash", { opacity: 0 }, { opacity: 1, ease: "none" }, 0)
Phone mockup zooms down into place
Jetson Charge, “The smartest thing in your garage, now in your pocket.”
- Triggerscroll, pinned
- Durationscrubbed
- Easingease-out cubic
- Movecopy −10vw, +10vh
OursScrollTrigger, GSAP
Details and recipe
- Trigger
- Overlaps 55vh; the finish is min(1, (0.55 × viewport height + 200) ÷ travel).
- Values
- Mockup x offset → 0, scale max(1, 0.9 × viewport height ÷ height) → 1 over 10–85% of the finish, ease-out cubic · copy from x −10vw, y +10vh and hidden to in place over 20–90% · button and note rise 16px at the end.
- Ours
- ScrollTrigger, GSAP. Recipe:
gsap.timeline({ scrollTrigger: { trigger: ".app", start: "top 55%", end: "bottom bottom", scrub: true } }) .fromTo(".phone", { x: dx, scale: 0.9 * innerHeight / h }, { x: 0, scale: 1, ease: "power3.out" }, 0.1) .fromTo(".copy", { x: -0.1 * innerWidth, y: 0.1 * innerHeight, autoAlpha: 0 }, { x: 0, y: 0, autoAlpha: 1 }, 0.2)
Impact card grows, rows step through
About, the Impact section.
- Triggerscroll, pinned
- Duration601px grow · 300ms rows
- Easingscrub · standard
- MovescaleX 0.3 → 1
OursScroll Stack, React Bits
Details and recipe
- Trigger
- Pinned; rows change at 46%, 61% and 89% of the travel.
- Values
- Card scaleX 0.3 × viewport width ÷ card width → 1 over the first 25% (about 600px), cubic-bezier(0.3333, 0, 0.6667, 1) · overlay reveals at 25–33% · each row step is a 300ms cubic-bezier(0.2, 0.7, 0.2, 1) translate; passed rows lift 36px and dim.
- Ours
- Scroll Stack, React Bits. Scroll Stack handles the row stepping (itemDistance 36, itemScale 0, baseScale 1); the card grow is one GSAP tween:
gsap.fromTo(".card", { scaleX: .3 }, { scaleX: 1, scrollTrigger: { trigger: ".impact", start: "top top", end: "+=25%", scrub: true } })
Frame scales up, crew photos rise
About, “Join the crew” (desktop only; the phone version clips each photo open).
- Triggerscroll, pinned
- Durationscrubbed
- Easinginout
- Movephotos rise 48px
OursScrollTrigger, GSAP
Details and recipe
- Trigger
- Pinned on a section with 192px top padding; phases remapped to the remaining travel.
- Values
- Frame scaleX min(1, 0.24 × vw ÷ w), scaleY min(1, 0.32 × vh ÷ h) → 1 over 2–40% · van rises 48px at 42–56% · crew at 56–70% · copy rises 32px at 70–84% · cubic-bezier(0.645, 0.045, 0.355, 1).
- Ours
- ScrollTrigger, GSAP. Recipe:
.fromTo(".frame", { scaleX: .24 * innerWidth / w, scaleY: .32 * innerHeight / h }, { scaleX: 1, scaleY: 1, ease: "power2.inOut" }, .02) .fromTo(".van", { y: 48, autoAlpha: 0 }, { y: 0, autoAlpha: 1 }, .42) .fromTo(".crew", { y: 48, autoAlpha: 0 }, { y: 0, autoAlpha: 1 }, .56)
Quote call to action dims as you scroll
“Get your custom price fast.” on Jetson Air, How it works, Service areas, Resources and Reviews.
- Triggerscroll, pinned
- Duration848px of travel
- Easinglinear
- Moveopacity 0.5 → 0.6
OursScrollTrigger, GSAP
Details and recipe
- Trigger
- Pinned over the section; at 50% the headline swaps for the address form.
- Values
- Scrim opacity 0.5 → 0.6, linear over the travel (about 850px) · reduced motion holds 0.6 · phone scrim 0.3.
- Ours
- ScrollTrigger, GSAP. Recipe. In our flow the address field is the action: it is on screen from the start and never swapped in by scroll.
gsap.fromTo(".dim", { opacity: .5 }, { opacity: .6, ease: "none", scrollTrigger: { trigger: ".quote-cta", start: "top top", end: "bottom bottom", scrub: true } })
Reveals
Things that appear once as they enter the viewport. Our Hint allowance is one quiet rise per section below the first screen.
Section rise
Headings and blocks on nine pages, for example “Half your energy goes to heating and cooling”.
- Triggerenters viewport (80%)
- Duration600ms
- Easingstandard
- Move16px rise · 50ms stagger
OursAnimated Content, React Bits
Details and recipe
- Trigger
- IntersectionObserver when the element's top passes 80% of the viewport; once; instant if already in view or at the page bottom; focus inside also reveals.
- Values
- translateY 16px → 0 and opacity 0 → 1 · 600ms · cubic-bezier(0.2, 0.7, 0.2, 1) · stagger 50ms per child, or 300ms in sequence mode.
- Ours
- Animated Content, React Bits. Config: distance 16, direction vertical, duration 0.6, ease power3.out, initialOpacity 0, threshold 0. Ours is 24px and once only: the one rise per section the Hint level allows.
Feature row and card stagger
Jetson Water and Charge, the system features row and the “Everything handled” cards.
- Triggerenters viewport (70%)
- Duration600 / 760ms
- Easingstandard
- Move24px rise · 160px slide · 100–150ms stagger
OursAnimated Content, React Bits
Details and recipe
- Trigger
- Reveal at 70% of the viewport; re-arms when scrolled back above it.
- Values
- Features y 24px → 0 and fade, 600ms, 900ms + 100ms per item · cards x 160px → 0 and fade, 760ms, 400ms + 150ms per item · cubic-bezier(0.2, 0.7, 0.2, 1).
- Ours
- Animated Content, React Bits. Config: distance 160, direction horizontal, duration 0.76, delay 0.4, stagger 0.15. We keep once true: nothing re-hides.
Hero staged load-in
Jetson Air, “Better air. All around.”
- Triggerload
- Duration600ms
- Easingstandard
- Movestages at 600 to 2,600ms
OursFade Content, React Bits Not adopted above the fold.
Details and recipe
- Trigger
- Timers after mount at 600, 1,500, 2,200 and 2,600ms; leaving the viewport or reduced motion jumps to the end.
- Values
- Opacity 0 → 1 per stage (title lines, rebate line, action, trust row) · 600ms · cubic-bezier(0.2, 0.7, 0.2, 1).
- Ours
- Fade Content, React Bits. Not adopted above the fold. Our first viewport never starts hidden and the action is never animated. Below the first screen: duration 600, easing ease-out, a delay per block.
Video plays when in view
Home hero; Jetson Air “One system. Every season.” and “Everything handled”.
- Triggerenters viewport (200px early)
- Moveplay / pause on intersect
OursScrollTrigger, GSAP
Details and recipe
- Trigger
- One observer 200px early mounts the source; a second plays on intersect and pauses when out of view.
- Values
- preload none · muted · loop · playsinline · reduced motion never loads the source, the poster stays.
- Ours
- ScrollTrigger, GSAP. No component needed:
ScrollTrigger.create({ trigger: video, start: "top bottom", end: "bottom top", onToggle: s => s.isActive ? video.play() : video.pause() })
Text
Words that arrive in pieces. Only below the first screen, and only on headlines.
Statement text, grapheme by grapheme
Jetson Air intro “One intelligent system”, the Water and Charge system lead, the home statements.
- Triggerenters viewport (70%)
- Duration200ms
- Easingstandard
- Move11.3ms per grapheme
OursSplit Text, React Bits
Details and recipe
- Trigger
- Reveal at 70% of the viewport; re-hides when scrolled back.
- Values
- Per grapheme opacity 0.5 (0.35 on dark, 0.45 on deep green) → 1 · 200ms · cubic-bezier(0.2, 0.7, 0.2, 1) · 11.3ms per grapheme, one extra step at each word boundary · the follow-up line starts 200ms after the last grapheme, then 400ms per bullet.
- Ours
- Split Text, React Bits. Config: splitType chars, from opacity 0.5, to 1, delay 11.3, duration 0.2, ease power2.out, rootMargin 0px 0px −30% 0px. Only on headlines below the first screen.
Intro overlay: lines appear, then the overlay lifts
How it works, before “We handle everything.”
- Triggerload, once per session
- Duration600ms fade · 1,250ms lift
- Easingstandard · move
- Movelines 800ms apart
OursSplit Text, React Bits Not adopted.
Details and recipe
- Trigger
- On load, once per session; lines 800ms apart; leaves by itself after 3,400ms; any click, key, wheel, touch or scroll skips it.
- Values
- Lines appear in sequence · exit by fade 600ms (300ms when skipped), cubic-bezier(0.2, 0.7, 0.2, 1), or by lift translateY −100% in 1,250ms, cubic-bezier(0.65, 0, 0.25, 1) · never shown under reduced motion.
- Ours
- Split Text, React Bits. Not adopted. A curtain before the first paint is what our motion rules forbid: the first paint is the real page. If a line-by-line arrival is wanted lower down: Split Text with lines, from opacity 0 and y 16, delay 800, duration 0.6.
Navigation
The header, its menus and the in-page nav.
Header hides on scroll down, narrows at the top
The fixed header on every page.
- Triggerscroll direction
- Duration600ms
- Easingstandard
- MovetranslateY −100%
OursHide On Scroll by Ozer (interior.dev), 21st.dev
Details and recipe
- Trigger
- Scroll, read each frame: always shown in the first 120px; hides after 12px down, returns after 8px up; narrows over the first 200px on desktop.
- Values
- translateY 0 → −100% · 600ms · cubic-bezier(0.2, 0.7, 0.2, 1) · the narrowing scrubs per frame.
- Ours
- Hide On Scroll by Ozer (interior.dev), 21st.dev. Config: thresholds 12 down and 8 up, showAbove 120, transition 0.6 with ease [0.2, 0.7, 0.2, 1].
Section nav with scroll-spy
The in-page navs on How it works, Heat pump FAQ and Resources.
- Triggersection link click
- Durationbrowser smooth, 1,500ms cap
- Moveactive at 40% of the viewport
OursSticky Header by Ozer (interior.dev), 21st.dev
Details and recipe
- Trigger
- A click turns on smooth scrolling until the scroll ends (1,500ms cap); the active link is the last section whose top is above 40% of the viewport.
- Values
- Native smooth scroll · class swap on the active link · no smoothing under reduced motion.
- Ours
- Sticky Header by Ozer (interior.dev), 21st.dev. The shell only; with Lenis the jump is lenis.scrollTo(target) and the spy is an IntersectionObserver with rootMargin 0px 0px −60% 0px.
Controls
Tabs, accordions, carousels and dialogs: the hand asks, the control answers.
Tabs: dot and colour swap
Jetson Air system components, Home app features, About “What Jetson does”, the FAQ.
- Triggerclick, arrow keys
- Durationinstant
- Movedot and colour swap
OursAnimated Tabs by Edu Calvo (SmoothUI), 21st.dev
Details and recipe
- Trigger
- Click or arrow keys.
- Values
- Label colour subtle → full · list dot opacity 0 → 1 · the panel swaps with no tween.
- Ours
- Animated Tabs by Edu Calvo (SmoothUI), 21st.dev. Indicator animation off to match the instant swap. This page's segment control is the same pattern.
Sticky panel with tabs bound to scroll steps
Jetson Air, “Half your energy goes to heating and cooling”.
- Triggerscroll snap, tab click
- Duration460–760ms
- Easingglide
- Movesteps 0.2 / 0.6 / 1 vh
OursAnimated Tabs by Build UI, 21st.dev
Details and recipe
- Trigger
- Snap points at 0.2, 0.6 and 1 of the viewport inside a sticky panel; choosing a tab glides to its step.
- Values
- Glide min(760, 460 + 0.3 × distance) ms · cubic-bezier(0.65, 0, 0.35, 1) · bars preset at 62, 71 and 67% · tabs only under reduced motion.
- Ours
- Animated Tabs by Build UI, 21st.dev. The tab list only; drive the value from ScrollTrigger snap [0.2, 0.6, 1] in onUpdate. No wheel capture.
Accordion: the plus turns, the answer drops in
FAQ blocks on eight pages (“Questions before you switch.”, “Jetson Care FAQs”).
- Triggerclick
- Duration300ms
- Easingstandard · reveal
- Move−8px → 0
OursInteractive Accordion by Jatin Yadav, 21st.dev
Details and recipe
- Trigger
- Click.
- Values
- Icon rotate 0 → 45° · 300ms · cubic-bezier(0.2, 0.7, 0.2, 1) · answer opacity 0 → 1 and −8px → 0 · 300ms · cubic-bezier(0.22, 1, 0.36, 1) · only for items the user opens · none under reduced motion.
- Ours
- Interactive Accordion by Jatin Yadav, 21st.dev. Config: transition duration 0.3, ease [0.22, 1, 0.36, 1], content from opacity 0 and y −8. The Origin UI accordion in the catalogue above gets the same timing.
Carousels on native scroll-snap
“Trusted by 2,500+ homes”, “We handle everything”, feature rows, reviews by city.
- Triggerdrag, arrows
- Durationbrowser smooth
- Movesnap-start · 4/5 width on phone
OursCarousel, React Bits
Details and recipe
- Trigger
- Horizontal scroll, a drag past 4px, or the arrows.
- Values
- snap-x mandatory, snap-start slides, four-fifths width on phone · smooth scroll when motion is allowed, instant under reduced motion · the active dot follows the nearest stop.
- Ours
- Carousel, React Bits. Config: autoplay false, loop false, round false; this is the drag-and-transform version. Jetson's native snap is lighter and what we prefer: CSS scroll-snap plus two arrow buttons.
Dialog and scrim fade
Spec dialogs, the address modal, the location picker.
- Triggeropen, close
- Duration300ms
- Easingease · standard
- Movescrim 0 → 0.2
OursFade Content, React Bits
Details and recipe
- Trigger
- Open and close.
- Values
- Dialog opacity 0 → 1 · 300ms · ease · scrim 0 → 0.2 · 300ms · cubic-bezier(0.2, 0.7, 0.2, 1) · none under reduced motion.
- Ours
- Fade Content, React Bits. Config: duration 300, easing ease, no blur, on the dialog body; the sheet in the catalogue above fades the same way.
Feedback
Numbers, pulses and spinners.
Savings count-up with a track wipe
The Jetson Water and Charge savings calculator.
- Triggerenters viewport (92%)
- Duration1,400ms
- Easingease-out cubic
- Moveafter 1,000ms
OursCount Up, React Bits
Details and recipe
- Trigger
- Reveal at 92% of the viewport; starts 1,000ms later; stops on focus or pointer.
- Values
- Value 0 → target · 1,400ms · ease-out cubic · track clip-path wipe 760ms, cubic-bezier(0.65, 0, 0.35, 1), after 250ms · label row rises 16px in 600ms · final value only under reduced motion.
- Ours
- Count Up, React Bits. Config: from 0, duration 1.4, delay 1, separator “,”. Ours counts only a number that changes (the price bar when a choice changes); a figure that is simply shown is set in place.
Map dots with a pulsing halo
Service areas, “Now installing near you.”
- Triggeralways
- Duration3,200ms loop
- Easingease-in-out
- Movescale 0.85 → 1.3
OursMarker by Wensity Labs, 21st.dev Not adopted.
Details and recipe
- Trigger
- Always; infinite.
- Values
- Opacity 0.4 → 0.8 → 0.4 and scale 0.85 → 1.3 → 0.85 · 3,200ms · ease-in-out · each dot offset by a golden-ratio fraction of the loop · none under reduced motion.
- Ours
- Marker by Wensity Labs, 21st.dev. Not adopted. A loop that never settles. The pulse variant is the match if a brief ever asks; ours is a still dot with a halo on hover.
Legacy keyframes still in the stylesheet
Jetson Air global CSS; nothing on the page uses them.
- Triggernone
- Duration300 / 780 / 800ms
- Easingglide
- Moveunused
OursFade Content, React Bits
Details and recipe
- Trigger
- None.
- Values
- Card select scale 0.95 → 1, 300ms · a 16px rise, 780ms, cubic-bezier(0.65, 0, 0.35, 1) · spinner 360° in 800ms, linear, infinite (the advisor and intake loading state).
- Ours
- Fade Content, React Bits. Nothing to adopt. A loading spinner is shown only past one second, and progress moves at an even pace.
Reduced motion
Jetson honours prefers-reduced-motion piece by piece: the scroll-timeline renders each scene's end state once and its sticky wrappers go static, the snap engine switches off, reveals drop their transitions, videos keep their posters and never load a source, the intro overlay is never shown and the halo stops. Two legacy pieces do not check it.
Ours is one rule, not a list. Every tween, pin and reveal lives inside gsap.matchMedia("(prefers-reduced-motion: no-preference)"); when the preference is set there is no Lenis and no tween, every scene renders its final frame in place, videos stay on their posters, and tabs and accordions swap instantly. Independent of the preference: nothing in the first viewport starts hidden, the action is never animated, and the page works with scripts off.
The price bar, working
Step six as a working fragment: three payment modes, one live monthly estimate, rebates netted as named lines, and the real price always shown next to the incentive price.
Sample figures for a 2.5-ton cold-climate system; replace with real pricing before any of this reaches a buyer.
$7,400 after incentives, paid in cash
Real price $14,900, installed. Rebates paid after install; the 25C credit comes back at tax time.
$88 /mo est., financed
$0 down, 7.49% APR, 120 months, on $7,400 after incentives · Edit terms
$89 /mo for 10 years, leased
Incentives captured by the owner and built into the payment. Maintenance and filters included. Buy-out option after year five.
| 2.5-ton cold-climate heat pump, Better tier, installed | $13,400 |
|---|---|
| Smart thermostat | $400 |
| Permit and old-unit disposal | $1,100 |
| Price | $14,900 |
| Federal 25C credit | −$2,000 |
| HEEHRA rebate, if eligible | −$4,000 |
| Xcel Energy rebate | −$1,500 |
| After incentives | $7,400 |
| Sales tax | Pending until the home visit |