ui-008

Pricing Slider

£10 or the whole Vault, £99

A pricing slider for SaaS plans: the price counts up as you drag, tiers snap magnetically, and a monthly/annual switch re-tweens the total.

intermediate
7 more details
count-upmicro-interactionsnapmagneticvalue-transitionkeyboard-navigationform-animation

This element reads better at your own screen size than in the frame below:

Updated

About this component

A pricing scrubber with the states a real pricing page needs. Tier ticks sit on the track, the thumb is magnetically pulled toward the nearest one as you drag and settles onto it with a spring, and the price counts between tiers rather than stepping. A value bubble follows the thumb on drag, on touch and on keyboard focus, the "most popular" tier lights up and pings as the thumb passes it, and a monthly / annual switch re-tweens the same number while a savings badge pops in with a percentage derived from the tier data.

Read the full effect overview

The tiers are read from the markup, so the same list is the source of truth, the legend under the track and the no-JavaScript fallback price list. Full keyboard support (arrows, Home, End, PageUp, PageDown) with aria-valuetext that announces the tier and its price rather than a raw index, a public API and CustomEvents for wiring the rest of a checkout, and coarse-pointer sizing with nothing hover-only. Light and dark themes are one design at two sets of custom properties, switched with a single data-variant attribute; the script is identical in both and never reads the attribute name.

Pricing Slider - UI component preview

What's included

12 items
  • Tier ticks on the track, with the thumb magnetically pulled to the nearest one as you drag
  • Release between tiers and it settles onto the tick with an elastic spring
  • Price counts between tiers through a numeric proxy tween instead of stepping
  • Value bubble tracks the thumb with the tier and price, on pointer, touch and keyboard focus
  • Monthly / annual switch re-tweens the price and pops a savings badge derived from the tier data
  • "Most popular" tier highlights, with a ring ping as the thumb passes it
  • Feature list restaggers as the tier changes
  • Arrows, Home, End, PageUp and PageDown, with aria-valuetext announcing the tier and price
  • Tiers read from the markup, so the legend doubles as a no-JavaScript price list
  • slider:change / slider:input / slider:billing events and a small public API
  • Coarse pointers get larger targets and a bubble that stays after release, nothing hover-only
  • Light and dark themes from one token list, switched with a single data-variant attribute

Perfect for

4 use cases
  • SaaS pricing pages with seat, usage or plan sliders
  • Calculators and quote widgets
  • Plan comparison and upgrade flows
  • Checkout steps where the plan is chosen before payment

How it works

4 sections

One state object, one render pass

The component holds a single state of {pos, mix}: the fractional position along the tiers and the blend between the monthly and annual price. Everything visible, the thumb, the fill, the bubble, the headline number, the tick highlights, is written by one render function reading that state, and GSAP only ever tweens the state. That is why a settle, a billing switch and a keyboard press cannot leave the component holding a stale value: they all animate the same two numbers.

Magnetic snap and the spring settle

While you drag, the raw slider value is biased toward the nearest tick by an amount that grows quadratically as you approach it, so the thumb is drawn in rather than sticking. On release the position is tweened to that tick with an elastic ease, and because the price is derived from the position it counts through the overshoot with it.

The number never jumps

The displayed price is interpolated from the surrounding tiers and then blended between the monthly and annual figure, so scrubbing and toggling the billing period both animate the same digits. The savings badge is computed from the tier data at load, never written into the copy, so changing a price in the markup changes the badge.

A native range under a painted one

The control is a real <input type="range"> at full opacity zero over the painted track, so pointer, touch and assistive technology get native behaviour, while the visible thumb, ticks and fill are ordinary elements GSAP can animate. Keys are handled explicitly so an arrow press moves a whole tier rather than one thousandth of the range, and aria-valuetext announces the tier name and its price.

Difficulty Intermediate
Includes HTML + JS + CSS source, documentation, AI setup prompt, lifetime updates

Lighthouse, as measured

Google Lighthouse on this UI element's demo, from the latest scan. A measurement of the demo as shipped, not a promise for your page.

Accessibility
100
Best practices
100

No performance score, on purpose. That figure depends on how you deploy: your server's compression and caching, your CDN, the connection and the device doing the test, none of which the code controls. The same page can score very differently on two consecutive runs, so measure it where it will live.

Paid UI element

Purchase to unlock the code.

Buying Pricing Slider opens the HTML, CSS and JavaScript source, the full documentation, an AI setup prompt for your editor, and every update we ship to it. Standard license: unlimited personal and commercial projects. Every paid product is tested for desktop and mobile layouts, reduced-motion handling, animation cleanup and keyboard controls wherever there is something to operate.

£10 Standard license, unlimited projects

Browse UI elements

Or the whole library: the Vault, £99, one payment.

Documentation

Quick Start

1. Add to your HTML <head>:

Code snippet omitted: it ships with the download.

2. Add before the closing </body> tag:

Code snippet omitted: it ships with the download.

3. Add the component markup:

Copy the [data-pricing-slider] block from index.html (it is marked with <!-- Component starts here -->). It needs:

  • [data-pricing-slider] root, optionally with data-start="1", data-billing="monthly" and data-currency="£"
  • a head showing [data-ps-tier], [data-ps-seats], [data-ps-price], [data-ps-period] and the [data-ps-popular] badge
  • the billing role="radiogroup" with [data-ps-seg-pill] and two [data-ps-bill="monthly|annual"] buttons, plus [data-ps-save] holding [data-ps-save-value]
  • the rail: [data-ps-input] (a real <input type="range">) before [data-ps-track], which holds [data-ps-fill], [data-ps-ticks] and [data-ps-thumb]
  • [data-ps-bubble] > [data-ps-bubble-inner] > [data-ps-bubble-tier] and [data-ps-bubble-price]
  • [data-ps-tiers]: the tier list, one [data-ps-tier-option] per tier
  • [data-ps-features] for the feature list and a role="status" element with [data-ps-live]

Everything except [data-ps-input], [data-ps-track], [data-ps-fill], [data-ps-thumb] and [data-ps-tiers] is optional: drop the billing switch, the bubble, the badge or the feature list and the script simply skips it. The .ps-cta button is plain markup with no hook — wire it to your own checkout.

The tier list is the data. Each option carries data-key, data-label, data-seats, data-monthly, data-annual, an optional data-popular, and a pipe-separated data-features. Nothing is hard-coded in the script: edit a price in the markup and the headline, the bubble, the legend, the ticks and the savings badge all follow.

Using It With Your Own Design

What the component needs from your markup: the data attributes above, and the range input as the first child of .ps-rail (the focus ring is a sibling selector on it). Ticks and legend positions are computed, so any number of tiers works.

What is only the demo's furniture: the fixed toolbar strip at the top of index.html (its prompt, the Reset button and the Light/Dark toggle, plus their CSS and inline script), the .stage / .stage__inner wrapper that centres the card on the grey ground, and the Google Font link. None of it is part of what you bought. Delete all of it and drop .pricing-slider straight into your own page.

CSS the component depends on: do not put a CSS transform on [data-ps-thumb], [data-ps-bubble-inner], [data-ps-seg-pill] or the tick dots — GSAP owns those, and a CSS transform underneath one is read as a pixel offset it never returns from. Keep --ps-thumb-size defined on the root: the script reads it at use time to map the value to a pixel, and the native range thumb is sized from the same property so the painted thumb and the real one stay on top of each other. Under .has-js the rail is shown and the legend becomes absolutely positioned markers; without JavaScript the rail is hidden and the same list renders as a plain price table.

What's Included

  • index.html: the component alone on the demo ground, with the theme toggle
  • assets/style.css: component CSS and both themes, every colour as a custom property
  • assets/script.js: readable GSAP core source with the keyboard model, cleanup and the public API
  • assets/script.min.js: minified production script
  • thumbnail.html: build-only capture state

Themes

One design at two token values. data-variant on <body> in the demo, or on the component's own owning root in a product page:

Code snippet omitted: it ships with the download.

light is the default; dark is the same layout, spacing and motion with the tokens re-valued. There is no third look, and the JavaScript never reads the attribute name — if it needs a colour it reads the custom property at use time, so a theme switch cannot leave the component holding a stale value. The component re-measures itself when the attribute changes, so a theme with different padding or fonts still lines its ticks up.

Both blocks in assets/style.css define the same list, and that list is the whole surface:

  • Ground (demo furniture only): --ground, --ground-2, --raised, --hover
  • Ink: --ink, --ink-2, --ink-3
  • Lines and elevation: --line, --line-strong, --shadow, --shadow-sm
  • Accent: --accent, --accent-ink, --accent-wash, --accent-ink-soft, --accent-hover
  • Status: --good-wash, --good-ink (the savings badge; muted, and used only for status)
  • Component surfaces: --track, --thumb-bg, --seg-bg, --seg-pill-bg, --bubble-bg, --bubble-ink
  • Type and radii: --font, --radius, --radius-sm, --radius-xs

To re-value it for your own brand, change those properties — not the rules under them. The usual edit is one line: set --accent (and --accent-hover, --accent-wash, --accent-ink-soft alongside it) to your brand colour and leave everything else alone.

How It Works

One state object, one render pass. The component holds {pos, mix}: the fractional position along the tiers, and the blend between the monthly and annual price. The thumb, the fill, the bubble, the headline number and the tick highlights are all written by a single render function reading that state, and GSAP only ever tweens the state. A settle, a billing switch and a keyboard press therefore cannot leave anything holding a stale value.

Magnetic snap, spring settle. While you drag, the raw value is biased toward the nearest tick by an amount that grows as you approach it, so the thumb is drawn in rather than sticking. On release the position tweens to that tick with an elastic ease, and the price counts through the overshoot with it.

The number never jumps. The price is interpolated between the surrounding tiers and then blended between the monthly and annual figure, so scrubbing and switching the billing period animate the same digits. The savings percentage is derived from the tier data at load and written into the badge; it is never copy.

A native range under a painted one. The control is a real <input type="range"> at opacity zero over the painted track, so pointer, touch and assistive technology all get native behaviour. Keys are handled explicitly so an arrow moves a whole tier rather than one thousandth of the range.

Public API and events. The first instance is exposed as window.pricingSlider (each root also carries its own as root.pricingSlider):

Code snippet omitted: it ships with the download.

The root dispatches three bubbling CustomEvents, each with the tier in detail:

Code snippet omitted: it ships with the download.

Pass { animate: false } to set or setBilling to apply without motion, and { silent: true } to apply without announcing or emitting.

Accessibility

  • The control is a native <input type="range">, so it is one tab stop, is exposed as a slider, and works with pointer, touch and assistive technology without a custom widget.
  • Keys: Left/Down move one tier down, Right/Up one tier up, PageDown two down, PageUp two up, Home the cheapest tier, End the most expensive. All are handled explicitly and clamped to the ends.
  • Announcement: aria-valuetext is rewritten on every change to the tier name, its price, the billing period and the seat count, so a screen reader says "Studio, £99 per user per month, 20 developers" rather than "2". aria-valuenow, aria-valuemin and aria-valuemax carry the tier index rather than the fractional drag position.
  • Settling on a tier also writes to a polite role="status" region, which covers the case where the value change came from a drag or the API rather than a key.
  • The billing switch is a role="radiogroup" of two role="radio" buttons with a roving tabindex: one tab stop, arrow keys and Home/End move between them, and aria-checked follows the selection immediately rather than waiting for the price tween.
  • The value bubble appears on focus as well as on pointer and touch, and is aria-hidden because everything in it is already announced.
  • The legend under the track is a pointer shortcut, not a second tab stop: every tier is reachable from the slider itself and from the API.
  • Coarse pointers get a 34px thumb, a 52px rail, 44px switch buttons and a bubble that lingers after release. Nothing in the component is hover-only.
  • Under prefers-reduced-motion: reduce the tier changes, the billing switch and the bubble all apply instantly, with no snap spring, no ping and no stagger.
  • Without JavaScript the rail is hidden and the tier list renders as a readable price table, so the page still sells the plans.

Requirements

  • GSAP 3.12+ (the demo uses 3.15.0; core only, no plugins)
  • No build step, no framework

Worked examples, the events and programmatic API, and the class reference ship with the download, alongside the full source.