Elastic Range
A forms and inputs field with a tethered exact-value bubble.
A small folded edition
Set the number of copies for this specimen.
Copies, in steps of ten in the default edition.
Two sheets per copy
Customise
Code
"use client";
import { useState } from "react";
import { InputElasticRange } from "@/components/input-elastic-range";
function EditionQuantity({
value = 40, min = 20, max = 200, step = 10,
elastic = true, disabled = false,
}: {
value?: number; min?: number; max?: number; step?: number;
elastic?: boolean; disabled?: boolean;
}) {
const [accepted, setAccepted] = useState({ value, min, max, step, quantity: value });
if (!Object.is(accepted.value, value) || !Object.is(accepted.min, min) ||
!Object.is(accepted.max, max) || !Object.is(accepted.step, step)) {
setAccepted({ value, min, max, step, quantity: value });
}
return (
<InputElasticRange
label="Edition quantity"
description="Copies; each copy uses two sheets."
min={min}
max={max}
step={step}
value={accepted.quantity}
onValueChange={(quantity) => setAccepted((current) => ({ ...current, quantity }))}
formatValue={(value) => `${value} copies`}
elastic={elastic}
disabled={disabled}
/>
);
}
<EditionQuantity
value={40}
min={20}
max={200}
step={10}
/>Elastic Range is a free React range input for Next.js and Tailwind. Its native thumb and form value update immediately, while a separate bubble follows with a bounded spring and displays the current committed number.
The field needs React 19 and React DOM 19 for its deduplicated static stylesheet. Installation includes the component, its private numeric model and the unchanged motion, duration-tokens and utils modules, with motion, clsx and tailwind-merge as package dependencies. The implementation and demo are original work.
Without JavaScript, the visible label, help, native range and numeric form submission remain available. Optional feedback stays hidden and formatted aria-valuetext is omitted. Supply a deterministic plain-text formatter: throwing, non-string or empty results fall back to the canonical number, while long text wraps within the field.
Use finite bounds from -1,000,000 to 1,000,000 with min below max, at most six decimal places, and a positive step from 0.000001 to the domain span. An invalid domain falls back as a whole to 0–100 with step 1. Values snap to the nearest step anchored at min, ties select the larger valid value, and an off-grid maximum stops at the last reachable step. Nonfinite supplied values use min; an omitted uncontrolled default uses the normalised midpoint.
Choose controlled or uncontrolled mode at mount and remount to change it. A controlled owner must commit requests through value; ignored requests do not become the displayed or submitted value. Uncontrolled defaultValue is captured once. Reset restores that initial number in the current domain, while controlled reset reapplies the latest committed owner value after the native default action. A prevented reset changes neither mode, and reset does not emit onValueChange.
The spring moves only the decorative bubble and tether, with the bubble constrained to the field and within 24 CSS pixels of its new target. Large changes may first project the bubble into that interval. Reduced motion, disabled, elastic=false, forced colours, print, hidden or offscreen states select static feedback; return snaps to the current value. The native control retains its own keyboard, pointer and form behaviour.
Install
Add the @te registry to your components.json once:
{
"registries": {
"@te": "https://templateempire.io/r/{name}.json"
}
}Then install the component:
npx shadcn@latest add @te/input-elastic-rangeUsage
"use client";
import { useState } from "react";
import { InputElasticRange } from "@/components/input-elastic-range";
export function EditionQuantity() {
const [quantity, setQuantity] = useState(40);
return (
<InputElasticRange
label="Edition quantity"
description="Copies; each copy uses two sheets."
min={20}
max={200}
step={10}
value={quantity}
onValueChange={setQuantity}
formatValue={(value) => `${value} copies`}
name="quantity"
/>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | required | Visible native label. Empty or whitespace-only text falls back to Value; meaningful text is preserved. |
| id | string | useId() | Native input id and label target. Caller-supplied nonempty ids must be page-unique. |
| description | string | undefined | Visible plain-text help associated through aria-describedby. Include units in the label or help for no-JS use. |
| min | number | 0 | Inclusive lower bound, subject to the documented bounded decimal domain. |
| max | number | 100 | Inclusive upper bound. An off-grid maximum need not be reachable. |
| step | number | 1 | Positive numeric step anchored at min; no step=any or logarithmic mapping. |
| value | number | undefined | Selects controlled mode at mount, including nonfinite numbers. Only committed owner values are displayed; a later absent value resolves to min. |
| defaultValue | number | normalised midpoint | Uncontrolled initial/reset value captured once. Later changes are ignored; value takes precedence in a controlled mount. |
| onValueChange | (value: number) => void | required | One notification per accepted native user input event. Prop changes, reset, hydration, focus and animation do not fabricate notifications. |
| name | string | undefined | Native form name. Without it the field contributes no FormData entry; formatted units are never submitted. |
| form | string | undefined | Native external form association. React-owned changes rebind reset bookkeeping to input.form. |
| disabled | boolean | false | Native disabled state. Omits the field from submission and normal focus navigation and stops feedback motion. |
| elastic | boolean | true | Enables optional bubble/tether feedback. False keeps the native field and a static exact-value bubble. |
| formatValue | (value: number) => string | canonical numeric string | Deterministic plain text shared by enhanced aria-valuetext and the decorative bubble. Empty, non-string or throwing results use the canonical number. |
| inputRef | Ref<HTMLInputElement> | undefined | Access to the actual native input for focus/read operations. Direct DOM value mutation is not a controlled update API. |
| className | string | undefined | Merged on the normal-flow wrapper. The consumer owns field width and surrounding layout. |
Customisation
Scope --elastic-range-track and --elastic-range-thumb for track, thumb border and tether ink; --elastic-range-surface for opaque thumb/bubble backing; --elastic-range-ink for bubble text and internal focus stroke; and --elastic-range-focus for the input outline. Their fallbacks use foreground/background or current/system colours at each paint property. Pair surface and ink in a nested scope and assess actual control, text and focus contrast. There is no duration or physics prop; feedback uses the unchanged house springSnappy preset.
.print-field {
--elastic-range-track: var(--foreground);
--elastic-range-thumb: var(--foreground);
--elastic-range-surface: var(--background);
--elastic-range-ink: var(--foreground);
--elastic-range-focus: var(--foreground);
max-inline-size: 28rem;
}Frequently asked questions
- Is Elastic Range free for commercial use?
- Yes. You may use and modify Elastic Range in personal, commercial and client products under MIT with the Commons Clause condition. Keep the copyright, licence and Commons Clause notices in the source. Selling or repackaging the components themselves as a catalogue, component library or UI kit requires a separate agreement.
- How does Elastic Range behave without motion or JavaScript?
- It keeps a real labelled native range with numeric form submission. Reduced motion selects static feedback. Without JavaScript the optional feedback remains hidden and no formatted accessible value becomes stale; include the unit context in the visible label or help.
- Can I customise units and the surrounding design?
- Yes. Provide a deterministic plain-text formatter and scope the five colour tokens. The consumer owns width, typography and surrounding layout. The number submitted to the form stays numeric, and the component does not add pricing, multiple thumbs or custom keyboard stepping.