Skip to content

Early previewhamur is not on npm yet. The components are being built in the open, follow along on GitHub.

InputsReact

Angle Slider

Picks an angle, such as a direction or a rotation, by turning a thumb around a dial.

Rendered by React 19.3.1

Loading React…
import { AngleSlider } from '@hamur/react/angle-slider'

const ticks = Array.from({ length: 12 }, (_, index) => index * 30)

export default function AngleSliderExample() {
  return (
    <AngleSlider.Root className="angle-slider" defaultValue={120}>
      <AngleSlider.Label>Rotation</AngleSlider.Label>
      <AngleSlider.Control className="angle-slider-control">
        <AngleSlider.Indicator className="angle-slider-indicator" />
        {ticks.map((value) => (
          <AngleSlider.Marker
            key={value}
            className="angle-slider-marker"
            value={value}
          />
        ))}
        <AngleSlider.Value className="angle-slider-value" />
        <AngleSlider.Thumb className="slider-thumb angle-slider-thumb" />
      </AngleSlider.Control>
    </AngleSlider.Root>
  )
}
Not measured yetTests passing in React
Not measured yetSize
Not measured yetMemory
Not measured yetSpeed
[N] commits

Installation

npm i @hamur/react

Not on npm yet. The package goes out under the chosen name.

Anatomy

Import the component and assemble its parts:

<AngleSlider.Root>
  <AngleSlider.Label />
  <AngleSlider.Control>
    <AngleSlider.Indicator />
    <AngleSlider.Marker />
    <AngleSlider.Value />
    <AngleSlider.Thumb />
  </AngleSlider.Control>
</AngleSlider.Root>

Examples

Controlled

A wind direction held in state with step 5; each change shows its reason, and a button sets north.

AngleSliderControlled.tsx
import * as React from 'react'
import { AngleSlider } from '@hamur/react/angle-slider'

const directions = [0, 45, 90, 135, 180, 225, 270, 315]

export default function AngleSliderControlledExample() {
  const [value, setValue] = React.useState(225)
  const [reason, setReason] = React.useState('none')
  return (
    <div className="stack">
      <AngleSlider.Root
        className="angle-slider"
        value={value}
        step={5}
        onValueChange={(next, details) => {
          setValue(next)
          setReason(details.reason)
        }}
      >
        <AngleSlider.Label>Wind direction</AngleSlider.Label>
        <AngleSlider.Control className="angle-slider-control">
          <AngleSlider.Indicator className="angle-slider-indicator" />
          {directions.map((direction) => (
            <AngleSlider.Marker key={direction} className="angle-slider-marker" value={direction} />
          ))}
          <AngleSlider.Value className="angle-slider-value" />
          <AngleSlider.Thumb className="slider-thumb angle-slider-thumb" />
        </AngleSlider.Control>
      </AngleSlider.Root>
      <p className="muted angle-slider-log">
        value {value} · reason {reason}
      </p>
      <button type="button" className="button" onClick={() => setValue(0)}>
        Point north
      </button>
    </div>
  )
}

Form

A heading in a Field inside a Form; the form submits the degrees by name.

AngleSliderForm.tsx
import * as React from 'react'
import { AngleSlider } from '@hamur/react/angle-slider'
import { Field } from '@hamur/react/field'
import { Form } from '@hamur/react/form'

export default function AngleSliderFormExample() {
  const [submitted, setSubmitted] = React.useState('')
  return (
    <Form
      className="stack"
      onFormSubmit={(_values, details) => {
        const form = details.event.target as HTMLFormElement
        setSubmitted([...new FormData(form)].map(([key, value]) => `${key}=${value}`).join('&'))
      }}
    >
      <Field.Root name="heading" className="field angle-slider-field">
        <Field.Label>Heading</Field.Label>
        <AngleSlider.Root className="angle-slider" defaultValue={90} step={5}>
          <AngleSlider.Control className="angle-slider-control">
            <AngleSlider.Indicator className="angle-slider-indicator" />
            <AngleSlider.Value className="angle-slider-value" />
            <AngleSlider.Thumb className="slider-thumb angle-slider-thumb" />
          </AngleSlider.Control>
        </AngleSlider.Root>
        <Field.Description className="muted">
          Arrows turn it by 5°, Shift and Page Up or Down by 15°.
        </Field.Description>
      </Field.Root>
      <button className="button" type="submit">
        Send
      </button>
      {submitted ? <pre className="output">{submitted}</pre> : null}
    </Form>
  )
}

API reference

20 props across 7 parts, in React syntax. className, style, render are listed once, under Shared props.

ControlAngleSlider.Control

The circle that takes presses and drags: the value follows the pointer's angle around its center. Renders a <div>.

IndicatorAngleSlider.Indicator

A layer the size of its box turned to the value, so a needle drawn at its top points at the value. Renders a <div>.

LabelAngleSlider.Label

An accessible label that names the angle slider's input and group. Renders a <div>.

MarkerAngleSlider.Marker

A decorative mark on the circle at its own value, hidden from assistive technology. Renders a <div>.

Marker props
PropTypeDefault
valuerequiredThe marker's place in degrees, normalised into one turn.number—
Marker data attributes
Data attributeDescription
data-activePresent when the marker is at the value.
data-filledPresent when the marker lies on the arc from 0 to the value.
data-draggingPresent while the user is dragging.
data-disabledPresent when the angle slider is disabled.
data-validPresent when the angle slider is in a valid state (when wrapped in Field.Root).
data-invalidPresent when the angle slider is in an invalid state (when wrapped in Field.Root).
data-touchedPresent when the angle slider has been touched (when wrapped in Field.Root).
data-dirtyPresent when the angle slider's value has changed (when wrapped in Field.Root).
data-focusedPresent when the angle slider is focused (when wrapped in Field.Root).
Marker CSS variables
CSS variableDescription
--angle-slider-marker-angleThe marker's own value in degrees.

RootAngleSlider.Root

Groups all parts of the angle slider: one value in degrees, 0 at the top and clockwise, wrapping at a full turn. Renders a <div>.

Root props
PropTypeDefault
defaultValueThe uncontrolled value in degrees when the slider is initially rendered,number0
valueThe value in degrees, 0 at the top and clockwise.number—
disabledWhether the slider should ignore user interaction.booleanfalse
formatOptions to format the value, merged over { style: 'unit', unit: 'degree', unitDisplay: 'narrow' }.Intl.NumberFormatOptions—
localeThe locale used by Intl.NumberFormat when formatting the value.Intl.LocalesArgument—
nameIdentifies the field when a form is submitted.string—
formIdentifies the form that owns the slider input.string—
stepThe degrees between positions: 0, step, 2·step, … below 360.number1
largeStepThe degrees moved by Page Up/Page Down and Shift + an arrow key, and the multiple a pointer snaps to while Shift is held.number15
onValueChangeCallback fired when the value changes.((value: number, eventDetails: AngleSliderRootChangeEventDetails) => void)—
onValueCommittedCallback fired when a change is committed: on release, per key, per input change.((value: number, eventDetails: AngleSliderRootCommitEventDetails) => void)—
Root data attributes
Data attributeDescription
data-draggingPresent while the user is dragging.
data-disabledPresent when the angle slider is disabled.
data-validPresent when the angle slider is in a valid state (when wrapped in Field.Root).
data-invalidPresent when the angle slider is in an invalid state (when wrapped in Field.Root).
data-touchedPresent when the angle slider has been touched (when wrapped in Field.Root).
data-dirtyPresent when the angle slider's value has changed (when wrapped in Field.Root).
data-focusedPresent when the angle slider is focused (when wrapped in Field.Root).
Root CSS variables
CSS variableDescription
--angle-slider-angleThe value in degrees, inherited by every part.

ThumbAngleSlider.Thumb

The thumb on the circle at the value. It holds the visually hidden <input type="range"> that takes focus and the keys. and a nested <input type="range">. Renders a <div>.

Thumb props
PropTypeDefault
aria-valuetextA string value forwarded to the [aria-valuetext](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-valuetext) attribute of the input.string—
getAriaValueTextA function which returns a string value for the [aria-valuetext](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-valuetext) attribute of the input.((formattedValue: string, value: number) => string) | null—
inputRefA ref to access the nested input element.React.Ref<HTMLInputElement>—
onBlurA blur handler forwarded to the input.React.FocusEventHandler<HTMLInputElement>—
onFocusA focus handler forwarded to the input.React.FocusEventHandler<HTMLInputElement>—
onKeyDownA keydown handler forwarded to the input.React.KeyboardEventHandler<HTMLInputElement>—
tabIndexOptional tab index attribute forwarded to the input.number—

ValueAngleSlider.Value

Displays the value as text: "90°" by default. Renders an <output>.

Value props
PropTypeDefault
childrenReplaces the text: receives the formatted value and the value in degrees.((formattedValue: string, value: number) => React.ReactNode) | null—

Shared props

Every part that renders an element takes these.

Shared props
PropType
classNameCSS class applied to the element, or a function that returns a class based on the component's state.string | ((state: State) => string | undefined)
styleStyle applied to the element, or a function that returns a style object based on the component's state.React.CSSProperties | ((state: State) => React.CSSProperties | undefined)
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, State>

Keyboard and accessibility

Keyboard

Keyboard
KeysDoesPart
ArrowRightArrowUpTurns clockwise by step, in right-to-left tooThumb
ArrowLeftArrowDownTurns counterclockwise by stepThumb
ShiftTurns by largeStep (15° by default)Thumb
PageUpPageDownTurns by largeStepThumb
HomeEndSets 0° or the last position (359° with step 1)Thumb
Wrap: 359° to 0° and 0° to 359°Thumb

Accessibility

What the parts set
PartSetsSo that
Thumba nested <input type="range"> with min="0", max the last position, aria-valuenowthe angle is read as a slider with its value, never outside its range
Thumbaria-valuetext with the long unit, or from getAriaValueTextthe value is read as "90 degrees", not "90" or a symbol
Thumbaria-labelledby to Label or the Field labelthe slider has a name
Rootrole="group", aria-labelledby to Labelthe dial is read as one named control
Markeraria-hidden="true"the ticks are drawn only; the value text already says where the thumb is
Valuerenders an <output> for the inputthe value text is tied to the slider without being announced on every change

Focus order

  1. The Thumb is one tab stop.
  2. Pressing the Control focuses the thumb; clicking Label focuses it too.