Angle Slider
An angle slider is a circular dial that allows users to select an angle, typically in degrees, within a 360° range. It provides an intuitive way to control rotations or orientations, offering accessibility features.
Features
- Fully managed keyboard navigation.
- Supports touch or click on track to update value.
- Supports Right-to-Left directionality.
Installation
To use the angle slider machine in your project, run the following command in your command line:
npm install @zag-js/angle-slider @zag-js/react # or yarn add @zag-js/angle-slider @zag-js/react
npm install @zag-js/angle-slider @zag-js/solid # or yarn add @zag-js/angle-slider @zag-js/solid
npm install @zag-js/angle-slider @zag-js/vue # or yarn add @zag-js/angle-slider @zag-js/vue
npm install @zag-js/angle-slider @zag-js/svelte # or yarn add @zag-js/angle-slider @zag-js/svelte
This command will install the framework agnostic angle slider logic and the reactive utilities for your framework of choice.
Anatomy
To set up the angle slider correctly, you'll need to understand its anatomy and how we name its parts.
Each part includes a
data-part
attribute to help identify them in the DOM.
Usage
First, import the angle-slider package into your project
import * as angleSlider from "@zag-js/angle-slider"
The angle slider package exports two key functions:
machine
— The state machine logic for the angle slider widget as described in the WAI-ARIA spec.connect
— The function that translates the machine's state to JSX attributes and event handlers.
You'll need to provide a unique
id
to theuseMachine
hook. This is used to ensure that every part has a unique identifier.
Next, import the required hooks and functions for your framework and use the angle slider machine in your project 🔥
import * as angleSlider from "@zag-js/angle-slider" import { normalizeProps, useMachine } from "@zag-js/react" export function AngleSlider() { const [state, send] = useMachine(angleSlider.machine({ id: "1" })) const api = angleSlider.connect(state, send, normalizeProps) return ( <div {...api.getRootProps()}> <label {...api.getLabelProps()}>Wind direction</label> <div {...api.getControlProps()}> <div {...api.getThumbProps()}></div> <div {...api.getMarkerGroupProps()}> {[0, 45, 90, 135, 180, 225, 270, 315].map((value) => ( <div key={value} {...api.getMarkerProps({ value })}></div> ))} </div> </div> <div {...api.getValueTextProps()}>{api.value} degrees</div> <input {...api.getHiddenInputProps()} /> </div> ) }
import * as angleSlider from "@zag-js/angle-slider" import { normalizeProps, useMachine } from "@zag-js/solid" import { createMemo, createUniqueId, Index } from "solid-js" export function AngleSlider() { const [state, send] = useMachine(angleSlider.machine({ id: createUniqueId() })) const api = createMemo(() => angleSlider.connect(state, send, normalizeProps)) return ( <div {...api().getRootProps()}> <label {...api().getLabelProps()}> Angle Slider: <div {...api().getValueTextProps()}>{api().valueAsDegree}</div> </label> <div {...api().getControlProps()}> <div {...api().getThumbProps()}></div> <div {...api().getMarkerGroupProps()}> <Index each={[0, 45, 90, 135, 180, 225, 270, 315]}> {(value) => <div {...api().getMarkerProps({ value: value() })}></div>} </Index> </div> </div> <input {...api().getHiddenInputProps()} /> </div> ) }
<script setup lang="ts"> import * as angleSlider from "@zag-js/angle-slider" import { normalizeProps, useMachine } from "@zag-js/vue" import { computed } from "vue" const [state, send] = useMachine(angleSlider.machine({ id: "1" })) const api = computed(() => angleSlider.connect(state.value, send, normalizeProps)) </script> <template> <div v-bind="api.getRootProps()"> <label v-bind="api.getLabelProps()"> Angle Slider: <div v-bind="api.getValueTextProps()">{{ api.valueAsDegree }}</div> </label> <div v-bind="api.getControlProps()"> <div v-bind="api.getThumbProps()"></div> <div v-bind="api.getMarkerGroupProps()"> <div v-for="value in [0, 45, 90, 135, 180, 225, 270, 315]" :key="value" v-bind="api.getMarkerProps({ value })" ></div> </div> </div> <input v-bind="api.getHiddenInputProps()" /> </div> </template>
<script lang="ts"> import * as angleSlider from "@zag-js/angle-slider" import { normalizeProps, useMachine } from "@zag-js/svelte" const [snapshot, send] = useMachine(angleSlider.machine({ id: "1" })) const api = $derived(angleSlider.connect(snapshot, send, normalizeProps)) </script> <div {...api.getRootProps()}> <label {...api.getLabelProps()}> Angle Slider: <div {...api.getValueTextProps()}>{api.valueAsDegree}</div> </label> <div {...api.getControlProps()}> <div {...api.getThumbProps()}></div> <div {...api.getMarkerGroupProps()}> {#each [0, 45, 90, 135, 180, 225, 270, 315] as value} <div {...api.getMarkerProps({ value })}></div> {/each} </div> </div> <input {...api.getHiddenInputProps()} /> </div>
Setting the initial value
const [state, send] = useMachine( angleSlider.machine({ value: 45, }), )
Setting the value's granularity
By default, the granularity, is 1
, meaning that the value is always an
integer. You can change the step attribute to control the granularity.
For example, If you need a value between 5
and 10
, accurate to two decimal
places, you should set the value of step to 0.01
:
const [state, send] = useMachine( angleSlider.machine({ step: 0.01, }), )
Listening for changes
When the angle slider value changes, the onValueChange
and onValueChangeEnd
callbacks are invoked. You can use this to setup custom behaviors in your app.
const [state, send] = useMachine( angleSlider.machine({ onValueChange(details) { console.log("value is changing to:", details) }, onValueChangeEnd(details) { console.log("value has changed to:", details) }, }), )
Usage within forms
To use angle slider within forms, use the exposed hiddenInputProps
from the connect
function and ensure you pass name
value to the machine's context. It will
render a hidden input and ensure the value changes get propagated to the form
correctly.
const [state, send] = useMachine( angleSlider.machine({ name: "wind-direction", }), )
RTL Support
The angle slider has built-in support for RTL alignment and interaction. In the RTL mode, operations are performed from right to left, meaning, the left arrow key will increment and the right arrow key will decrement.
To enable RTL support, pass the dir: rtl
context property
const [state, send] = useMachine( angleSlider.machine({ dir: "rtl", }), )
While we take care of the interactions in RTL mode, you'll have to ensure you apply the correct CSS styles to flip the layout.
Using angle slider marks
To show marks or ticks along the angle slider track, use the exposed
api.getMarkerProps()
method to position the angle slider marks at desired angles.
//... <div {...api.getRootProps()}> <label {...api.getLabelProps()}>Wind direction</label> <div {...api.getControlProps()}> <div {...api.getThumbProps()}></div> <div {...api.getMarkerGroupProps()}> {[0, 45, 90, 135, 180, 225, 270, 315].map((value) => ( <div key={value} {...api.getMarkerProps({ value })}></div> ))} </div> </div> <div {...api.getValueTextProps()}>{api.value} degrees</div> <input {...api.getHiddenInputProps()} /> </div> //...
//... <div {...api().getRootProps()}> <label {...api().getLabelProps()}>Wind direction</label> <div {...api().getControlProps()}> <div {...api().getThumbProps()}></div> <div {...api().getMarkerGroupProps()}> {[0, 45, 90, 135, 180, 225, 270, 315].map((value) => ( <div key={value} {...api().getMarkerProps({ value })}></div> ))} </div> </div> <div {...api().getValueTextProps()}>{api().value} degrees</div> <input {...api().getHiddenInputProps()} /> </div> //...
//... <div v-bind="api.getRootProps()"> <label v-bind="api.getLabelProps()"> Angle Slider: <div v-bind="api.getValueTextProps()">{{ api.valueAsDegree }}</div> </label> <div v-bind="api.getControlProps()"> <div v-bind="api.getThumbProps()"></div> <div v-bind="api.getMarkerGroupProps()"> <div v-for="value in [0, 45, 90, 135, 180, 225, 270, 315]" :key="value" v-bind="api.getMarkerProps({ value })" ></div> </div> </div> <input v-bind="api.getHiddenInputProps()" /> </div> //...
<!-- ... --> <div {...api.getRootProps()}> <label {...api.getLabelProps()}>Wind direction</label> <div {...api.getControlProps()}> <div {...api.getThumbProps()}></div> <div {...api.getMarkerGroupProps()}> {[0, 45, 90, 135, 180, 225, 270, 315].map((value) => ( <div key={value} {...api.getMarkerProps({ value })}></div> ))} </div> </div> <div {...api.getValueTextProps()}>{api.value} degrees</div> <input {...api.getHiddenInputProps()} /> </div> <!-- ... -->
Styling guide
Earlier, we mentioned that each angle slider part has a data-part
attribute added to
them to select and style them in the DOM.
Disabled State
When the angle slider is disabled, the data-disabled
attribute is added to the root,
label, control, thumb and marker.
[data-part="root"][data-disabled] { /* styles for root disabled state */ } [data-part="label"][data-disabled] { /* styles for label disabled state */ } [data-part="control"][data-disabled] { /* styles for control disabled state */ } [data-part="thumb"][data-disabled] { /* styles for thumb disabled state */ } [data-part="range"][data-disabled] { /* styles for thumb disabled state */ }
Invalid State
When the slider is invalid, the data-invalid
attribute is added to the root,
track, range, label, and thumb parts.
[data-part="root"][data-invalid] { /* styles for root invalid state */ } [data-part="label"][data-invalid] { /* styles for label invalid state */ } [data-part="control"][data-invalid] { /* styles for control invalid state */ } [data-part="valueText"][data-invalid] { /* styles for output invalid state */ } [data-part="thumb"][data-invalid] { /* styles for thumb invalid state */ } [data-part="marker"][data-invalid] { /* styles for marker invalid state */ }
Styling the markers
[data-part="marker"][data-state="(at|under|over)-value"] { /* styles for when the value exceeds the marker's value */ }
Methods and Properties
Machine Context
The slider machine exposes the following context properties:
ids
Partial<ElementIds>
The ids of the elements in the machine. Useful for composition.step
number
The step value for the slider.value
number
The value of the slider.onValueChange
(details: ValueChangeDetails) => void
The callback function for when the value changes.onValueChangeEnd
(details: ValueChangeDetails) => void
The callback function for when the value changes ends.disabled
boolean
Whether the slider is disabled.readOnly
boolean
Whether the slider is read-only.invalid
boolean
Whether the slider is invalid.name
string
The name of the slider. Useful for form submission.dir
"ltr" | "rtl"
The document's text/writing direction.id
string
The unique identifier of the machine.getRootNode
() => ShadowRoot | Node | Document
A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.
Machine API
The slider api
exposes the following methods:
value
number
The current value of the angle slidervalueAsDegree
string
The current value as a degree stringsetValue
(value: number) => void
Sets the value of the angle sliderdragging
boolean
Whether the slider is being dragged.
Data Attributes
Keyboard Interactions
- ArrowRightIncrements the angle slider based on defined step
- ArrowLeftDecrements the angle slider based on defined step
- ArrowUpIncreases the value by the step amount.
- ArrowDownDecreases the value by the step amount.
- PageUpIncreases the value by a larger step
- PageDownDecreases the value by a larger step
- Shift + ArrowUpIncreases the value by a larger step
- Shift + ArrowDownDecreases the value by a larger step
- HomeSets the value to 0 degrees.
- EndSets the value to 360 degrees.
Edit this page on GitHub