Buy flow · design language

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.

Read the nine steps

Internal reference · 2026-10-27 Sources: jetsonhome.com captures, a live read of tesla.com/modely/design, Tesla secondary sources Screenshot gallery

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.

  1. 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
    Start my price
    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 page on a phone: headline 'Let's Build your Custom Quote', a card with three green check marks, an address field with a green arrow button and the vertical five-dot progress rail.
    Jetson /intake, phone, empty state. jetsonhome.com/intake
  2. 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
    Size my system
    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 card '1. See your price. Enter your address and answer a few quick questions about your home', with the edge of the next card '2. Upload a few photos'.
    Jetson homepage, "Everything handled" step cards. jetsonhome.com/us/air
  3. 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
    Continue with the recommended size
    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 right rail: Cash, Lease and Finance tabs, a Recommended card at $166/mo, a Plan for Future Use card at $198/mo with '1 new EV. Edit', Customize, Compare Payment Types and a sticky bar reading $198/mo Est. Payment with an Order Now button.
    Tesla Energy design rail, Lease tab selected. electrek.co
  4. 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
    Continue
    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).

    Tesla Model Y configurator on a phone: three trim cards priced per month, the terms line '$3,999 (8%) down, 5.29% APR, 60 months', a checked box 'Include est. incentives of $8,000 and gas savings of $100/mo for New York', and the sticky bar '$613/mo after est. savings' with Order Now.
    Model Y trim cards on the Finance tab. reddit.com r/TeslaModelY
  5. 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
    Apply my rebates
    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 price breakdown: solar panels and Powerwall line items, two discount lines in negative, Cash Price $37,671 'Taxes and fees not included', Electricity Provider, Est. Payment $198/mo with its terms line, and Due Today $100 'Refundable deposit until you accept your design'.
    Tesla Energy breakdown and deposit copy. electrek.co
  6. 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
    Continue with finance (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 'Compare Payment Types' with three columns: Cash $47,211 system price, Lease $265/mo with an optional buyout after five years, Finance $388/mo on $42,490 financed with an editable down payment, a term dropdown and 7.24% APR; each column shows its own 25-year savings.
    Tesla Energy "Compare Payment Types". reddit.com r/TeslaSolar
  7. 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
    Hold this week
    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 homepage closing card: 'Get your custom price fast.' over a green Jetson van on a street, an 'Enter your home address' field and a green 'Get your price' button.
    Jetson's closing address card, phone. jetsonhome.com/us/air
  8. 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
    Pay the $100 deposit
    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).

    Tesla Model Y order summary on a phone: Est. Payment $864/mo, 'After Federal Tax Credit if eligible $721/mo', the terms line, Mobile Charger $250, Sales Tax Pending, Non-Refundable Order Fee $250, Due Today $500, an Apple Pay button and an 'Order with Card' button.
    Model Y summary: fees itemized, Due Today, Apple Pay then card. reddit.com r/TeslaModelY
  9. 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
    Place order
    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 Model 3 configurator rail with a checked row '$10,500 Est. Net Trade-In Credit, View Details' under the trim cards and the sticky bar reading '$26,490 Vehicle Price, Includes est. trade-in' with Order Now.
    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.

  1. 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.
  2. 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.
  3. 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).
  4. 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.
  5. Live price figure

    Steps three to eight. Tesla's instantly updating price.

    Component
    Number Flow, SmoothUI (Edu Calvo), rolling digits on the number-flow package. 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.
  6. 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: sticky block 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).
  7. 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-3 wash plus a check glyph, never an accent outline (frontend draft guard). Each card shows its own $/mo in the selected payment mode.
  8. 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.
  9. 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".
  10. 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.
  11. 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.
  12. 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-3 wash.
  13. 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.
  14. 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-2 inside the field. Fuel, ducts and existing AC are radio cards, not selects.
  15. 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-2 field.
  16. 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.
  17. 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.
  18. 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-3 and the ink, figures in Oswald above them, assumptions in a Small footnote (rate, degree days, fuel price), no grid lines, no tooltip.
  19. 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() on prefers-reduced-motion: no-preference.
  20. 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

Ground #F3EEE4 · warm paper
Ground step #EAE3D6 · panels
Selected wash #E0D8C8 · pressed, chosen
Ink #1B1A17 · text, art, rules
Ink soft #5A564D · captions, 6.4:1
Accent #7C4F17 bronze · the action, links, 6.1:1

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.

Oswald 400 caps, display 48 to 104, labels 500 caps at 0.12em

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.

Durations
TokenJetsonOurs (GSAP seconds)Jetson uses it for
fast200ms0.2Statement text per grapheme; the dialog scrim; menu items leaving.
base300ms0.3Accordion, tabs, nav underline, dialog, header narrowing, row steps.
slow600ms0.6Section rise, feature stagger, header hide, menu items arriving, the hero load-in.
glide-short460msnot used (no snap)The shortest snap glide.
glide-long760ms0.76 for the long revealsThe longest snap glide; the card stagger; the track wipe.
held-11250msnot used (nothing takes more than a second)The phone menu sweep; the intro overlay lift.
Easing
TokenJetsonOurs (GSAP ease)Jetson uses it for
revealcubic-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.
emphasiscubic-bezier(0.5, 0, 0.1, 1)power3.inOut; only for a scene changeThe phone menu sweep.
standardcubic-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.
movecubic-bezier(0.65, 0, 0.25, 1)power2.inOut; only for a scene changeThe intro overlay lift.
glidecubic-bezier(0.65, 0, 0.35, 1)not used (no snap)Snap glides, the phone-side scene phases, the track wipe.
scrubcubic-bezier(0.3333, 0, 0.6667, 1)none: scrubbed scenes track the scrollbarWhole-scene progress on the Air scenes.
inoutcubic-bezier(0.645, 0.045, 0.355, 1)power2.inOutThe careers frame.
  1. revealcubic-bezier(0.22, 1, 0.36, 1)
  2. emphasiscubic-bezier(0.5, 0, 0.1, 1)
  3. standardcubic-bezier(0.2, 0.7, 0.2, 1)
  4. movecubic-bezier(0.65, 0, 0.25, 1)
  5. scrubcubic-bezier(0.3333, 0, 0.6667, 1)
  6. 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.

Jetson Air · desktop 1440 · 10 s · The install window grows to the full frame, then the right-size photo shrinks into a framed card as the scroll goes on.
Jetson Air · phone 390 · 9 s · The same install scene on a phone: the window scales up from a tile, then the dim and the copy follow.
  1. Scroll-timeline engine

    Every pinned scene on the home, Air, About, Water and Charge pages.

    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 } })
  2. Scroll snap to scene steps

    Pinned scenes with steps on the home, Air, About and How it works pages, desktop only (over 640px).

    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" }
  3. Hero video clips into a window

    Home, “Your home. Upgraded.”

    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)
  4. Products row travels sideways

    Home, the three product cards under “Jetson brings heating, hot water and EV charging together”.

    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] } })
  5. Title lifts while the hub slides under it

    Home, “The brains.”

    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 } })
  6. Collage slides in, centre window changes

    Home, “The next generation of your home.”

    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)
  7. App card expands to near full screen

    Home, “Your home, in your hands.”

    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).
  8. Quote card widens from the centre column

    Home, “Get your custom price fast.”

    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)
  9. Install window grows to the full frame

    Jetson Air, “Jetson Air installation”, after “We do it all.”

    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.
  10. App window grows to full bleed

    Jetson Air, “Your whole home. In your hand.”

    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.
  11. Photo shrinks into a framed card, columns slide in

    Jetson Air, “Not oversized. Just the right size.”

    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" } }
  12. Window scales up as two words split (phone only)

    Jetson Air, “Cleaner air, built in.”

    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))
  13. 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.”

    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" })
  14. Reserve call to action: window opens, a wash fades in

    Jetson Water and Charge, “Reserve your Jetson … today.”

    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)
  15. Phone mockup zooms down into place

    Jetson Charge, “The smartest thing in your garage, now in your pocket.”

    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)
  16. Impact card grows, rows step through

    About, the Impact section.

    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 } })
  17. Frame scales up, crew photos rise

    About, “Join the crew” (desktop only; the phone version clips each photo open).

    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)
  18. Quote call to action dims as you scroll

    “Get your custom price fast.” on Jetson Air, How it works, Service areas, Resources and Reviews.

    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.

Jetson Air · desktop 1440 · 8 s · The hero's staged load-in, then the intro statement and the first section rise as the scroll starts.
  1. Section rise

    Headings and blocks on nine pages, for example “Half your energy goes to heating and cooling”.

    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.
  2. Feature row and card stagger

    Jetson Water and Charge, the system features row and the “Everything handled” cards.

    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.
  3. Hero staged load-in

    Jetson Air, “Better air. All around.”

    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.
  4. Video plays when in view

    Home hero; Jetson Air “One system. Every season.” and “Everything handled”.

    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.

  1. Statement text, grapheme by grapheme

    Jetson Air intro “One intelligent system”, the Water and Charge system lead, the home statements.

    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.
  2. Intro overlay: lines appear, then the overlay lifts

    How it works, before “We handle everything.”

    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.

  1. Header hides on scroll down, narrows at the top

    The fixed header on every page.

    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].
  2. Phone menu sweeps in with colour bands

    The menu button below desktop widths, on every page.

    Trigger
    Open and close; the menu stays mounted 1,000ms while closing.
    Values
    Panel translateY −100% → 0 over 1,250ms, cubic-bezier(0.5, 0, 0.1, 1) · four 80px bands scaleY 5 → 1, settled at 72% · items opacity and 12px rise, 600ms, cubic-bezier(0.22, 1, 0.36, 1), from 500ms to 1,110ms · out in 600ms after 220ms · bands hidden under reduced motion.
    Ours
    Staggered Menu, React Bits. Position top, retimed to the values above. Ours sweeps one ink and a tone step: never four colour bands.
  3. Section nav with scroll-spy

    The in-page navs on How it works, Heat pump FAQ and Resources.

    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.
  4. Legacy navbar hide (Framer Motion)

    Old layouts only: the intake shell.

    Trigger
    Scroll direction.
    Values
    y −180 to hide, 0 to show · 300ms · cubic-bezier(0.4, 0, 0.2, 1) · a shared-layout morph on the address arrow · reduced motion not handled.
    Ours
    Hide On Scroll by Ozer (interior.dev), 21st.dev. Not adopted (legacy). Hide On Scroll above covers it.

Controls

Tabs, accordions, carousels and dialogs: the hand asks, the control answers.

  1. Tabs: dot and colour swap

    Jetson Air system components, Home app features, About “What Jetson does”, the FAQ.

    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.
  2. Sticky panel with tabs bound to scroll steps

    Jetson Air, “Half your energy goes to heating and cooling”.

    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.
  3. Accordion: the plus turns, the answer drops in

    FAQ blocks on eight pages (“Questions before you switch.”, “Jetson Care FAQs”).

    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.
  4. Dialog and scrim fade

    Spec dialogs, the address modal, the location picker.

    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.

  1. Savings count-up with a track wipe

    The Jetson Water and Charge savings calculator.

    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.
  2. Map dots with a pulsing halo

    Service areas, “Now installing near you.”

    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.
  3. Legacy keyframes still in the stylesheet

    Jetson Air global CSS; nothing on the page uses them.

    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.

Price lines
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 taxPending until the home visit

$88 /mo est.

$0 down, 7.49% APR, 120 months