Discrete Snap-Stop Slider — HTML CSS JS Snippet

Discrete Snap-Stop Slider · Forms · Plain HTML, CSS & JS · Live preview

What's included

Features

Holds a discrete index, not a continuous value — physically cannot land between two labeled stops
Every input method (drag, click stop, click label, arrow key) converges on one setIndex function
Dragging rounds live pointer position to the nearest stop, with the thumb easing smoothly to it
Clickable stop dots and clickable text labels, both jumping directly to that exact position
Full keyboard support — Left/Right/Up/Down step one stop, Home/End jump to the first/last
Real ARIA slider semantics with aria-valuetext announcing the current label, not just a raw number

About this UI Snippet

Discrete Snap-Stop Slider — An Index, Not a Continuous Value

Screenshot of the Discrete Snap-Stop Slider snippet rendered live

A price-range slider outputs any number between its min and max. This one is a completely different kind of control: it holds an *index* into a fixed list of labeled options — Standard, Express, Overnight, Same-Day — and can only ever land on exactly one of those four positions. There is no such thing as "index 1.5" here; every interaction, whatever triggers it, ends by calling setIndex() with a whole number.

Percentage is derived from the index, never the reverse

render() computes the thumb's visual position with one line: pct = (current / (count - 1)) * 100. Dragging works the opposite direction — it converts a raw pointer position into a percentage, then *rounds* that percentage into the nearest whole index with Math.round(pct * (count - 1)), and only that rounded index is ever written to current. The thumb visually eases to the exact stop position via a CSS transition on left, but the underlying state was never anything other than a clean integer.

Every input path converges on one function

Clicking a stop dot, clicking a text label, dragging the thumb, and pressing an arrow key are four completely different gestures, but every one of them ends by calling setIndex(i) — which is the only place that clamps the value into range and re-renders. That's what guarantees the slider can never visually or logically land between two stops, no matter which input method was used.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain why every interaction path — dragging, clicking a stop, clicking a label, pressing an arrow key — is written to funnel through one single setIndex function rather than each updating the thumb position independently, and what bug that pattern prevents as more input methods get added. It's also worth asking the assistant to make the stops unevenly spaced (e.g. logarithmic) by replacing the even-percentage formula with a per-stop lookup table, or to add a live tooltip above the thumb showing the current label while dragging.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a discrete, stop-only slider in plain HTML, CSS, and vanilla JavaScript that can only ever land on one of a small fixed set of labeled positions — never a continuous in-between value — no library.

Requirements:
- A plain JavaScript array of label strings representing the available discrete options (e.g. shipping speed tiers), with a "current index" integer variable representing which one is selected — this index must be the single source of truth, never a raw pixel or percentage position.
- A visual track with small stop-marker dots positioned at even percentage intervals matching the number of labels, a fill bar showing progress from the start to the current stop, and a draggable thumb.
- A render function that computes the thumb's and fill bar's percentage position purely by dividing the current index by (number of options minus 1) — this must be the only place visual position is calculated from the index.
- Dragging the thumb must track the pointer's live position, convert it to a percentage of the track's width, round that percentage to the nearest valid stop index in real time (not only on release), and immediately update the current index to that rounded value so the thumb visibly eases toward and settles on the nearest stop rather than following the cursor to an arbitrary in-between position.
- Clicking any stop dot, or clicking a text label naming that option, must jump the current index directly to that exact stop.
- Keyboard support: with the thumb focused, Left/Down arrow keys decrement the index and Right/Up arrow keys increment it (both clamped within valid range), and Home/End jump to the first and last index respectively.
- The thumb must carry proper ARIA slider role and attributes, including a human-readable aria-valuetext reflecting the currently selected label, not just a raw numeric value.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Source Code

<div class="dss-wrap">
  <label class="dss-label">Shipping speed</label>
  <div class="dss-track-wrap">
    <div class="dss-track">
      <div class="dss-fill" id="dssFill"></div>
      <button class="dss-stop" data-i="0" style="left:0%"></button>
      <button class="dss-stop" data-i="1" style="left:33.33%"></button>
      <button class="dss-stop" data-i="2" style="left:66.66%"></button>
      <button class="dss-stop" data-i="3" style="left:100%"></button>
      <div class="dss-thumb" id="dssThumb" tabindex="0" role="slider" aria-valuemin="0" aria-valuemax="3" aria-valuenow="0"></div>
    </div>
  </div>
  <div class="dss-ticks" id="dssTicks"></div>
</div>

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA 4-stop slider appears, set to "Standard" with the label highlighted below it.
  2. 2
    Drag the thumbIt follows your cursor smoothly, but always eases to and settles on the nearest labeled stop — never in between.
  3. 3
    Click a dot or labelJump straight to that exact stop with a smooth transition.
  4. 4
    Use arrow keysFocus the thumb and press Left/Right to move one stop at a time; Home/End jump to the first or last.
  5. 5
    Add a fifth tierAdd one label to LABELS and one .dss-stop button positioned at its new percentage — the math adjusts automatically.

Real-world uses

Common Use Cases

Shipping speed selectors
The exact pattern this snippet models — letting a shopper pick a tier, not an arbitrary number.
Plan or tier pickers
Slide between Free/Pro/Team/Enterprise as clean discrete steps rather than a dropdown.
Difficulty or intensity settings
Easy/Medium/Hard/Expert — any setting that's inherently a small fixed set of named options.
Multi-step progress selectors
Let a user jump directly to a specific onboarding or wizard step by dragging or clicking.

Got questions?

Frequently Asked Questions

Add one string to the LABELS array and one matching .dss-stop button in the HTML, positioned at its new even percentage (100 / (new count - 1) per step) — count updates automatically from LABELS.length.

Yes — give each .dss-stop button a custom left percentage instead of even spacing, and replace the render() percentage formula with a lookup table mapping each index to its actual custom percentage.

Rounding live (during the drag, not just at the end) is what makes the fill bar and any live label always show the stop you're about to land on, giving immediate feedback rather than a jarring correction only at release.

LABELS[current] gives the selected label at any time; hook a callback or custom event dispatch inside setIndex if you need to react to changes elsewhere in a real application.

Yes — the thumb carries role="slider" with aria-valuemin/max/now plus aria-valuetext for the human-readable label, and is fully operable via Left/Right/Up/Down/Home/End without a mouse.