A styled wrapper around <input type="range"> that shares the feedback, size, and background vocabulary of the rest of the form surface. The default fader look is a full control-height row: a rounded fill grows to a thin handle bar, with the label and a live value readout inside the row. It follows the same control sizes as Input and Button, so it lines up with them in forms.
In a form, label the fader with a Field like any other control. The row is split into zones: the track (fill, handle and step dots) runs between the text segments, so the handle and dots never pass under the value. The readout reserves room for its widest value, so the track doesn’t shift as the number changes. Combine with appearance="stepped" and marks for dotted, snapping stops. When the value lands on a labelled mark, the readout shows the mark’s label instead of the number (“Medium” below). In dual mode, the start value reads on the left and the end value on the right.
Stepped: dots per step, snaps to them.
---
// GENERATED from packages/demos/input-range/fader.demo.tsx — edit the source, not this file.
import InputRange from "@pindoba/astro-input-range";
import Field from "@pindoba/astro-field";
import { stack } from "@pindoba/styled-system/patterns";
const percent = { style: "percent", locale: "en-US" } as const;
const usd = {
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
locale: "en-US",
} as const;
const qualities = [
{ value: 0, label: "Low" },
{ value: 1, label: "Medium" },
{ value: 2, label: "High" },
];
---
<div
class={stack({
gap: "md",
direction: "column",
width: "32rem",
maxWidth: "100%",
})}
>
<Field label="Opacity">
<InputRange
value={0.4}
min={0}
max={1}
step={0.01}
formatValue={percent}
fullWidth
/>
</Field>
<Field label="Columns" description="Stepped: dots per step, snaps to them.">
<InputRange
value={3}
min={1}
max={6}
step={1}
appearance="stepped"
marks
fullWidth
/>
</Field>
<Field label="Quality">
<InputRange
value={1}
min={0}
max={2}
step={1}
appearance="stepped"
marks={qualities}
fullWidth
/>
</Field>
<Field label="Price">
<InputRange
valueStart={200}
valueEnd={750}
min={0}
max={1000}
step={10}
formatValue={usd}
fullWidth
/>
</Field>
</div>For compact settings panels, pass label to put the label in the row’s leading segment. It also becomes the input’s accessible name. Use origin="center" for signed ranges.
Inline segments hug their text, so the tracks of a stack of rows start at different points. To line them up, use labelPosition below.
labelPosition="top" (or "bottom") turns the label and the live value into a header line, with the full-width track below (or above) it. Every track in a stack then starts and ends at the same place, which suits settings and inspector panels. Dual ranges work the same way: the start value on the left and the end value on the right, or the label on the left and start – end on the right.
---
// GENERATED from packages/demos/input-range/label-position.demo.tsx — edit the source, not this file.
import InputRange from "@pindoba/astro-input-range";
import { stack } from "@pindoba/styled-system/patterns";
const usd = {
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
locale: "en-US",
} as const;
const grouped = { locale: "en-US" } as const;
---
<div
class={stack({
gap: "md",
direction: "column",
width: "32rem",
maxWidth: "100%",
})}
>
<InputRange
label="Rotation speed"
labelPosition="top"
value={3.1}
min={0}
max={5}
step={0.1}
fullWidth
/>
<InputRange
label="Spiral arms"
labelPosition="top"
value={4}
min={1}
max={6}
step={1}
appearance="stepped"
marks
fullWidth
/>
<InputRange
label="Stars"
labelPosition="top"
value={4000}
min={0}
max={10000}
step={100}
formatValue={grouped}
fullWidth
/>
<InputRange
labelPosition="top"
valueStart={200}
valueEnd={750}
min={0}
max={1000}
step={10}
formatValue={usd}
fullWidth
/>
<InputRange
label="Hours"
labelPosition="bottom"
valueStart={9}
valueEnd={17}
min={0}
max={24}
minGap={1}
fullWidth
/>
</div>---
// GENERATED from packages/demos/input-range/inline-label.demo.tsx — edit the source, not this file.
import InputRange from "@pindoba/astro-input-range";
import { stack } from "@pindoba/styled-system/patterns";
const qualities = [
{ value: 0, label: "Low" },
{ value: 1, label: "Medium" },
{ value: 2, label: "High" },
];
---
<div
class={stack({
gap: "xs",
direction: "column",
width: "32rem",
maxWidth: "100%",
})}
>
<InputRange label="Volume" value={62} min={0} max={100} fullWidth />
<InputRange
label="Speed"
value={1}
min={0.5}
max={2}
step={0.25}
appearance="stepped"
marks
fullWidth
/>
<InputRange
label="Balance"
value={0}
min={-50}
max={50}
step={10}
origin="center"
appearance="stepped"
marks
fullWidth
/>
<InputRange
label="Quality"
value={1}
min={0}
max={2}
step={1}
appearance="stepped"
marks={qualities}
fullWidth
/>
</div>formatValue formats the readout and the input’s aria-valuetext. Pass a function ((n) => n + "%") or Intl.NumberFormat options, optionally with a locale. In Astro, prefer the options form: it is serializable, so the client script keeps formatting as the value changes. A function only formats the server render there.
variant="track" keeps the classic thin rail with a round thumb, a primary tint and a transparent row. Prefix/suffix columns and tick labels are track-only layouts. Most demos below use it.
Apply semantic feedback colors to tint the filled track and thumb — useful for tying a slider to a validation state or a themed control group.
---
// GENERATED from packages/demos/input-range/feedback.demo.tsx — edit the source, not this file.
import InputRange from "@pindoba/astro-input-range";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "md", direction: "column" })}>
<InputRange
label="Neutral"
feedback="neutral"
value={40}
min={0}
max={100}
fullWidth
/>
<InputRange
label="Primary"
feedback="primary"
value={55}
min={0}
max={100}
fullWidth
/>
<InputRange
label="Success"
feedback="success"
value={70}
min={0}
max={100}
fullWidth
/>
<InputRange
label="Danger"
feedback="danger"
value={25}
min={0}
max={100}
fullWidth
/>
<InputRange
label="Warning"
feedback="warning"
value={85}
min={0}
max={100}
fullWidth
/>
</div>Four sizes follow the control size scale: the fader row matches the height, padding, and radius of an Input or Button of the same size. In the track variant, they scale the rail height and thumb diameter together.
---
// GENERATED from packages/demos/input-range/size.demo.tsx — edit the source, not this file.
import InputRange from "@pindoba/astro-input-range";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "md", direction: "column" })}>
<InputRange
label="Extra small"
size="xs"
value={20}
min={0}
max={100}
fullWidth
/>
<InputRange label="Small" size="sm" value={40} min={0} max={100} fullWidth />
<InputRange label="Medium" size="md" value={60} min={0} max={100} fullWidth />
<InputRange label="Large" size="lg" value={80} min={0} max={100} fullWidth />
</div>Use fieldPrefix and fieldSuffix to add labels or a live value readout alongside the track. The native <input> still owns the value — bind it to display the current number. Use labelPosition to place the labels inline with the track (default), or stacked on top or bottom.
---
// GENERATED from packages/demos/input-range/prefix-suffix.demo.tsx — edit the source, not this file.
import InputRange from "@pindoba/astro-input-range";
import { stack } from "@pindoba/styled-system/patterns";
import { css } from "@pindoba/styled-system/css";
const volume = 35;
const brightness = 72;
---
<div
class={stack({ gap: "lg", direction: "column" })}
data-ir-root="input-range/prefix-suffix"
>
<InputRange
variant="track"
aria-label="Volume"
value={35}
min={0}
max={100}
fullWidth
data-ir="1"
>
<span slot="field-prefix" class={css({ whiteSpace: "nowrap" })}>
Volume
</span>
<span
slot="field-suffix"
class={css({ minWidth: "3ch", textAlign: "right" })}
>
<!--ir:2-->
{volume}
<!--/ir-->%
</span>
</InputRange>
<InputRange
variant="track"
aria-label="Brightness"
value={72}
min={0}
max={100}
step={5}
fullWidth
labelPosition="top"
data-ir="3"
>
<span slot="field-prefix">Brightness</span>
<span slot="field-suffix" class={css({ textAlign: "right" })}>
<!--ir:4-->
{brightness}
<!--/ir-->%
</span>
</InputRange>
<InputRange
variant="track"
aria-label="Value"
value={60}
min={0}
max={100}
fullWidth
labelPosition="bottom"
data-ir="5"
>
<span slot="field-prefix">0</span>
<span slot="field-suffix" class={css({ textAlign: "right" })}>
100
</span>
</InputRange>
</div>
<script>
import { hydrate } from "./_client/prefix-suffix.ts";
document
.querySelectorAll('[data-ir-root="input-range/prefix-suffix"]')
.forEach((root) => hydrate([root]));
</script>Provide valueStart and valueEnd (instead of a single value) to select an interval with two thumbs — ideal for price ranges or bounds. In the fader, the start value reads on the left and the end value on the right. With a label, the label takes the left side and the interval reads as start – end on the right. Set minGap to keep a minimum distance between the thumbs. Each thumb is a real native <input type="range">, so keyboard and assistive-tech support come for free.
---
// GENERATED from packages/demos/input-range/dual.demo.tsx — edit the source, not this file.
import InputRange from "@pindoba/astro-input-range";
import { stack } from "@pindoba/styled-system/patterns";
const usd = {
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
locale: "en-US",
} as const;
---
<div
class={stack({
gap: "md",
direction: "column",
width: "28rem",
maxWidth: "100%",
})}
>
<InputRange
valueStart={200}
valueEnd={750}
min={0}
max={1000}
step={10}
formatValue={usd}
fullWidth
/>
<InputRange
label="Hours"
valueStart={9}
valueEnd={17}
min={0}
max={24}
minGap={1}
fullWidth
/>
<InputRange
variant="track"
valueStart={30}
valueEnd={60}
min={0}
max={100}
minGap={10}
feedback="success"
showValue="always"
fullWidth
/>
</div>Pass marks to render dots along the track: marks (with a step) auto-generates evenly-spaced marks, or pass an array of { value, label? } to place explicit, labelled marks.
---
// GENERATED from packages/demos/input-range/marks.demo.tsx — edit the source, not this file.
import InputRange from "@pindoba/astro-input-range";
import { stack } from "@pindoba/styled-system/patterns";
---
<div
class={stack({
gap: "lg",
direction: "column",
width: "28rem",
maxWidth: "100%",
})}
>
<InputRange
variant="track"
aria-label="Value"
value={40}
min={0}
max={100}
step={20}
marks
fullWidth
/>
<InputRange
variant="track"
aria-label="Speed"
value={50}
min={0}
max={100}
step={25}
marks={[
{ value: 0, label: "Faster" },
{ value: 100, label: "Smarter" },
]}
showValue
fullWidth
/>
</div>appearance="stepped" gives the segmented look — a wider pill thumb, firmer dots, and snap-to-mark defaulted on. Combine with marks and a step.
---
// GENERATED from packages/demos/input-range/stepped.demo.tsx — edit the source, not this file.
import InputRange from "@pindoba/astro-input-range";
import { stack } from "@pindoba/styled-system/patterns";
---
<div
class={stack({
gap: "lg",
direction: "column",
width: "28rem",
maxWidth: "100%",
})}
>
<InputRange
variant="track"
aria-label="Speed"
value={50}
min={0}
max={100}
step={25}
appearance="stepped"
marks
fullWidth
>
<span slot="field-prefix">Faster</span>
<span slot="field-suffix">Smarter</span>
</InputRange>
</div>Set snap with an array of marks (and step="any" for fine input resolution) to snap the value to the nearest mark on drag or arrow-key — including unevenly-spaced stops.
---
// GENERATED from packages/demos/input-range/snap.demo.tsx — edit the source, not this file.
import InputRange from "@pindoba/astro-input-range";
import { stack } from "@pindoba/styled-system/patterns";
import { css } from "@pindoba/styled-system/css";
const stops = [0, 10, 25, 60, 100];
const level = 25;
---
<div
class={stack({
gap: "sm",
direction: "column",
width: "28rem",
maxWidth: "100%",
})}
data-ir-root="input-range/snap"
>
<span class={css({ fontSize: "sm", color: "neutral.text.muted" })}>
Level: <!--ir:1-->
{level}
<!--/ir-->
</span>
<InputRange
variant="track"
aria-label="Value"
value={25}
min={0}
max={100}
step="any"
snap
marks={stops.map((value) => ({ value, label: String(value) }))}
fullWidth
data-ir="2"
/>
</div>
<script>
import { hydrate } from "./_client/snap.ts";
document
.querySelectorAll('[data-ir-root="input-range/snap"]')
.forEach((root) => hydrate([root]));
</script>The passThrough prop provides escape hatches for each slot: style accepts a Panda CSS SystemStyleObject and props forwards arbitrary HTML attributes to the corresponding element.
---
// GENERATED from packages/demos/input-range/custom.demo.tsx — edit the source, not this file.
import InputRange from "@pindoba/astro-input-range";
import { stack } from "@pindoba/styled-system/patterns";
import { css } from "@pindoba/styled-system/css";
const opacity = 50;
---
<div
class={stack({ gap: "xl", direction: "column" })}
data-ir-root="input-range/custom"
>
<div>
<h3>Wrapped in a surface via passThrough</h3>
<InputRange
variant="track"
aria-label="Custom surface"
value={40}
min={0}
max={100}
fullWidth
passThrough={{
root: {
style: {
background: "neutral.surface.hill",
borderRadius: "md",
padding: "md",
},
},
}}
data-ir="1"
/>
</div>
<div>
<h3>Squared thumb via passThrough</h3>
<InputRange
variant="track"
aria-label="Squared thumb"
value={65}
min={0}
max={100}
fullWidth
passThrough={{
input: {
style: {
"&::-webkit-slider-thumb": { borderRadius: "xs" },
"&::-moz-range-thumb": { borderRadius: "xs" },
},
},
}}
data-ir="2"
/>
</div>
<div>
<h3>Forwarded ARIA attributes</h3>
<InputRange
variant="track"
value={50}
min={0}
max={100}
fullWidth
labelPosition="top"
passThrough={{
input: {
props: {
"aria-label": "Opacity",
"data-testid": "opacity-slider",
},
},
}}
data-ir="3"
>
<span slot="field-prefix" class={css({ fontWeight: "medium" })}>
Opacity
</span>
<span slot="field-suffix" class={css({ textAlign: "right" })}>
<!--ir:4-->
{opacity}
<!--/ir-->%
</span>
</InputRange>
</div>
</div>
<script>
import { hydrate } from "./_client/custom.ts";
document
.querySelectorAll('[data-ir-root="input-range/custom"]')
.forEach((root) => hydrate([root]));
</script>