beta

useColorSlider

Provides the behavior and accessibility implementation for a color slider component. Color sliders allow users to adjust an individual channel of a color value.

installyarn add @react-aria/color
version3.0.0-beta.5
usageimport {useColorSlider} from '@react-aria/color'

API#


useColorSlider( (props: ColorSliderAriaOptions, , state: ColorSliderState )): ColorSliderAria

Features#


The <input type="color"> HTML element can be used to build a color picker, however it is very inconsistent across browsers and operating systems and consists of a complete color picker rather than a single color channel slider. useColorSlider helps achieve accessible and touch-friendly color sliders that can be styled as needed.

  • Support for adjusting a single channel of RGBA, HSLA, and HSBA colors
  • Support for mouse, touch, and keyboard via the useMove hook
  • Multi-touch support for dragging multiple sliders at once
  • Pressing on the track moves the thumb to that position
  • Supports using the arrow keys, as well as page up/down, home, and end keys
  • Support for both horizontal and vertical orientations
  • Support for custom step values with handling for rounding errors
  • Support for disabling the color slider
  • Prevents text selection while dragging
  • Exposed to assistive technology as a slider element via ARIA
  • Uses a hidden native input element to support touch screen readers
  • Automatic ARIA labeling using localized channel names by default
  • Support for visually labeling the slider
  • Support for displaying the current value using an <output> element
  • Internationalized number formatting based on the color channel type
  • Support for mirroring in RTL locales

Anatomy#


Color slider anatomy diagramThumbTrackOutputLabelLabelHue230°

A color slider consists of a track element and a thumb that the user can drag to change a single channel of a color value. It may also include optional label and <output> elements to display the color channel name and current numeric value, respectively. A visually hidden <input> element is used to represent the value to assistive technologies.

useColorSlider returns props that you should spread onto the appropriate elements:

NameTypeDescription
labelPropsHTMLAttributes<HTMLElement>Props for the label element.
trackPropsHTMLAttributes<HTMLElement>Props for the track element.
thumbPropsHTMLAttributes<HTMLElement>Props for the thumb element.
inputPropsHTMLAttributes<HTMLElement>Props for the visually hidden range input element.
outputPropsHTMLAttributes<HTMLElement>Props for the output element, displaying the value of the color slider.

State is managed by the useColorSliderState hook from @react-stately/color. The state object should be passed as an option to useColorSlider

By default, useColorSlider provides an aria-label for the localized color channel name. If you wish to display a visual label, or override this with a more specific label, a label, aria-label or aria-labelledby prop may be passed instead.

Example#


This example shows how to build a horizontal color slider. It also includes a label which can be clicked to focus the thumb. Styling for the track background and positioning of the thumb are provided by useColorSlider in the returned style prop for each element.

If no label prop is given, it uses the Color object to get a localized string for the channel name using the getChannelName method. In addition, an <output> element is used to display the current channel value as text. This is formatted using the Color object's formatChannelValue method, which formats the value according to the channel type and locale settings.

The <input> element inside the thumb is used to represent the color slider to assistive technology, and is hidden from view using the VisuallyHidden component. The thumb also uses the useFocusRing hook to grow in size when it is keyboard focused (try tabbing to it).

import {useColorSliderState} from '@react-stately/color';
import {VisuallyHidden} from '@react-aria/visually-hidden';
import {useLocale} from '@react-aria/i18n';
import {useFocusRing} from '@react-aria/focus';

const TRACK_THICKNESS = 28;
const THUMB_SIZE = 20;

function ColorSlider(props) {
  let {locale} = useLocale();
  let state = useColorSliderState({...props, locale});
  let trackRef = React.useRef();
  let inputRef = React.useRef();

  // Default label to the channel name in the current locale
  let label = props.label || state.value.getChannelName(props.channel, locale);

  let {
    trackProps,
    thumbProps,
    inputProps,
    labelProps,
    outputProps
  } = useColorSlider(
    {
      ...props,
      label,
      trackRef,
      inputRef
    },
    state
  );

  let {focusProps, isFocusVisible} = useFocusRing();

  return (
    <div
      style={{
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        width: 300
      }}>
      {/* Create a flex container for the label and output element. */}
      <div style={{display: 'flex', alignSelf: 'stretch'}}>
        <label {...labelProps}>{label}</label>
        <output {...outputProps} style={{flex: '1 0 auto', textAlign: 'end'}}>
          {state.value.formatChannelValue(props.channel, locale)}
        </output>
      </div>
      {/* The track element holds the visible track line and the thumb. */}
      <div
        {...trackProps}
        ref={trackRef}
        style={{
          ...trackProps.style,
          height: TRACK_THICKNESS,
          width: '100%',
          borderRadius: 4
        }}>
        <div
          {...thumbProps}
          style={{
            ...thumbProps.style,
            top: TRACK_THICKNESS / 2,
            border: '2px solid white',
            boxShadow: '0 0 0 1px black, inset 0 0 0 1px black',
            width: isFocusVisible ? TRACK_THICKNESS + 4 : THUMB_SIZE,
            height: isFocusVisible ? TRACK_THICKNESS + 4 : THUMB_SIZE,
            borderRadius: '50%',
            boxSizing: 'border-box',
            background: state.getDisplayColor().toString('css')
          }}>
          <VisuallyHidden>
            <input ref={inputRef} {...inputProps} {...focusProps} />
          </VisuallyHidden>
        </div>
      </div>
    </div>
  );
}

<ColorSlider channel="hue" defaultValue="hsl(0, 100%, 50%)" />
import {useColorSliderState} from '@react-stately/color';
import {VisuallyHidden} from '@react-aria/visually-hidden';
import {useLocale} from '@react-aria/i18n';
import {useFocusRing} from '@react-aria/focus';

const TRACK_THICKNESS = 28;
const THUMB_SIZE = 20;

function ColorSlider(props) {
  let {locale} = useLocale();
  let state = useColorSliderState({...props, locale});
  let trackRef = React.useRef();
  let inputRef = React.useRef();

  // Default label to the channel name in the current locale
  let label =
    props.label ||
    state.value.getChannelName(props.channel, locale);

  let {
    trackProps,
    thumbProps,
    inputProps,
    labelProps,
    outputProps
  } = useColorSlider(
    {
      ...props,
      label,
      trackRef,
      inputRef
    },
    state
  );

  let {focusProps, isFocusVisible} = useFocusRing();

  return (
    <div
      style={{
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        width: 300
      }}>
      {/* Create a flex container for the label and output element. */}
      <div style={{display: 'flex', alignSelf: 'stretch'}}>
        <label {...labelProps}>{label}</label>
        <output
          {...outputProps}
          style={{flex: '1 0 auto', textAlign: 'end'}}>
          {state.value.formatChannelValue(
            props.channel,
            locale
          )}
        </output>
      </div>
      {/* The track element holds the visible track line and the thumb. */}
      <div
        {...trackProps}
        ref={trackRef}
        style={{
          ...trackProps.style,
          height: TRACK_THICKNESS,
          width: '100%',
          borderRadius: 4
        }}>
        <div
          {...thumbProps}
          style={{
            ...thumbProps.style,
            top: TRACK_THICKNESS / 2,
            border: '2px solid white',
            boxShadow:
              '0 0 0 1px black, inset 0 0 0 1px black',
            width: isFocusVisible
              ? TRACK_THICKNESS + 4
              : THUMB_SIZE,
            height: isFocusVisible
              ? TRACK_THICKNESS + 4
              : THUMB_SIZE,
            borderRadius: '50%',
            boxSizing: 'border-box',
            background: state
              .getDisplayColor()
              .toString('css')
          }}>
          <VisuallyHidden>
            <input
              ref={inputRef}
              {...inputProps}
              {...focusProps}
            />
          </VisuallyHidden>
        </div>
      </div>
    </div>
  );
}

<ColorSlider
  channel="hue"
  defaultValue="hsl(0, 100%, 50%)"
/>
import {useColorSliderState} from '@react-stately/color';
import {VisuallyHidden} from '@react-aria/visually-hidden';
import {useLocale} from '@react-aria/i18n';
import {useFocusRing} from '@react-aria/focus';

const TRACK_THICKNESS = 28;
const THUMB_SIZE = 20;

function ColorSlider(
  props
) {
  let {
    locale
  } = useLocale();
  let state = useColorSliderState(
    {...props, locale}
  );
  let trackRef = React.useRef();
  let inputRef = React.useRef();

  // Default label to the channel name in the current locale
  let label =
    props.label ||
    state.value.getChannelName(
      props.channel,
      locale
    );

  let {
    trackProps,
    thumbProps,
    inputProps,
    labelProps,
    outputProps
  } = useColorSlider(
    {
      ...props,
      label,
      trackRef,
      inputRef
    },
    state
  );

  let {
    focusProps,
    isFocusVisible
  } = useFocusRing();

  return (
    <div
      style={{
        display: 'flex',
        flexDirection:
          'column',
        alignItems:
          'center',
        width: 300
      }}>
      {/* Create a flex container for the label and output element. */}
      <div
        style={{
          display:
            'flex',
          alignSelf:
            'stretch'
        }}>
        <label
          {...labelProps}>
          {label}
        </label>
        <output
          {...outputProps}
          style={{
            flex:
              '1 0 auto',
            textAlign:
              'end'
          }}>
          {state.value.formatChannelValue(
            props.channel,
            locale
          )}
        </output>
      </div>
      {/* The track element holds the visible track line and the thumb. */}
      <div
        {...trackProps}
        ref={trackRef}
        style={{
          ...trackProps.style,
          height: TRACK_THICKNESS,
          width: '100%',
          borderRadius: 4
        }}>
        <div
          {...thumbProps}
          style={{
            ...thumbProps.style,
            top:
              TRACK_THICKNESS /
              2,
            border:
              '2px solid white',
            boxShadow:
              '0 0 0 1px black, inset 0 0 0 1px black',
            width: isFocusVisible
              ? TRACK_THICKNESS +
                4
              : THUMB_SIZE,
            height: isFocusVisible
              ? TRACK_THICKNESS +
                4
              : THUMB_SIZE,
            borderRadius:
              '50%',
            boxSizing:
              'border-box',
            background: state
              .getDisplayColor()
              .toString(
                'css'
              )
          }}>
          <VisuallyHidden>
            <input
              ref={
                inputRef
              }
              {...inputProps}
              {...focusProps}
            />
          </VisuallyHidden>
        </div>
      </div>
    </div>
  );
}

<ColorSlider
  channel="hue"
  defaultValue="hsl(0, 100%, 50%)"
/>

Vertical#

This example shows how to build a vertical color slider. The main difference from horizontal color sliders is the addition of the orientation: 'vertical' option to useColorSlider. This automatically adjusts the internal positioning and dragging logic. Additionally, this example does not have a visible label or <output> element. This can be done by simply not using the returned labelProps and outputProps. The color slider will have a default aria-label using the localized channel name, which can be overridden by passing an aria-label prop to useColorSlider.

function ColorSlider(props) {
  let {locale} = useLocale();
  let state = useColorSliderState({...props, locale});
  let trackRef = React.useRef();
  let inputRef = React.useRef();
  let {groupProps, trackProps, thumbProps, inputProps} = useColorSlider(
    {
      ...props,
      orientation: 'vertical',
      trackRef,
      inputRef
    },
    state
  );

  let {focusProps, isFocusVisible} = useFocusRing();

  return (
    <div
      {...groupProps}
      style={{
        height: 200
      }}>
      <div
        {...trackProps}
        ref={trackRef}
        style={{
          ...trackProps.style,
          width: TRACK_THICKNESS,
          height: '100%',
          borderRadius: 4
        }}>
        <div
          {...thumbProps}
          style={{
            ...thumbProps.style,
            left: TRACK_THICKNESS / 2,
            border: '2px solid white',
            boxShadow: '0 0 0 1px black, inset 0 0 0 1px black',
            width: isFocusVisible ? TRACK_THICKNESS + 4 : THUMB_SIZE,
            height: isFocusVisible ? TRACK_THICKNESS + 4 : THUMB_SIZE,
            borderRadius: '50%',
            boxSizing: 'border-box',
            background: state.getDisplayColor().toString('css')
          }}>
          <VisuallyHidden>
            <input ref={inputRef} {...inputProps} {...focusProps} />
          </VisuallyHidden>
        </div>
      </div>
    </div>
  );
}

<ColorSlider channel="hue" defaultValue="hsl(0, 100%, 50%)" />
function ColorSlider(props) {
  let {locale} = useLocale();
  let state = useColorSliderState({...props, locale});
  let trackRef = React.useRef();
  let inputRef = React.useRef();
  let {
    groupProps,
    trackProps,
    thumbProps,
    inputProps
  } = useColorSlider(
    {
      ...props,
      orientation: 'vertical',
      trackRef,
      inputRef
    },
    state
  );

  let {focusProps, isFocusVisible} = useFocusRing();

  return (
    <div
      {...groupProps}
      style={{
        height: 200
      }}>
      <div
        {...trackProps}
        ref={trackRef}
        style={{
          ...trackProps.style,
          width: TRACK_THICKNESS,
          height: '100%',
          borderRadius: 4
        }}>
        <div
          {...thumbProps}
          style={{
            ...thumbProps.style,
            left: TRACK_THICKNESS / 2,
            border: '2px solid white',
            boxShadow:
              '0 0 0 1px black, inset 0 0 0 1px black',
            width: isFocusVisible
              ? TRACK_THICKNESS + 4
              : THUMB_SIZE,
            height: isFocusVisible
              ? TRACK_THICKNESS + 4
              : THUMB_SIZE,
            borderRadius: '50%',
            boxSizing: 'border-box',
            background: state
              .getDisplayColor()
              .toString('css')
          }}>
          <VisuallyHidden>
            <input
              ref={inputRef}
              {...inputProps}
              {...focusProps}
            />
          </VisuallyHidden>
        </div>
      </div>
    </div>
  );
}

<ColorSlider
  channel="hue"
  defaultValue="hsl(0, 100%, 50%)"
/>
function ColorSlider(
  props
) {
  let {
    locale
  } = useLocale();
  let state = useColorSliderState(
    {...props, locale}
  );
  let trackRef = React.useRef();
  let inputRef = React.useRef();
  let {
    groupProps,
    trackProps,
    thumbProps,
    inputProps
  } = useColorSlider(
    {
      ...props,
      orientation:
        'vertical',
      trackRef,
      inputRef
    },
    state
  );

  let {
    focusProps,
    isFocusVisible
  } = useFocusRing();

  return (
    <div
      {...groupProps}
      style={{
        height: 200
      }}>
      <div
        {...trackProps}
        ref={trackRef}
        style={{
          ...trackProps.style,
          width: TRACK_THICKNESS,
          height: '100%',
          borderRadius: 4
        }}>
        <div
          {...thumbProps}
          style={{
            ...thumbProps.style,
            left:
              TRACK_THICKNESS /
              2,
            border:
              '2px solid white',
            boxShadow:
              '0 0 0 1px black, inset 0 0 0 1px black',
            width: isFocusVisible
              ? TRACK_THICKNESS +
                4
              : THUMB_SIZE,
            height: isFocusVisible
              ? TRACK_THICKNESS +
                4
              : THUMB_SIZE,
            borderRadius:
              '50%',
            boxSizing:
              'border-box',
            background: state
              .getDisplayColor()
              .toString(
                'css'
              )
          }}>
          <VisuallyHidden>
            <input
              ref={
                inputRef
              }
              {...inputProps}
              {...focusProps}
            />
          </VisuallyHidden>
        </div>
      </div>
    </div>
  );
}

<ColorSlider
  channel="hue"
  defaultValue="hsl(0, 100%, 50%)"
/>

Usage#


The following examples show how to use the ColorSlider component created in the above example.

RGBA#

This example shows how you could build an RGBA color picker using four color sliders bound to the same color value in state. The parseColor function is used to parse the initial color from a hex value, stored in state. The value and onChange props of ColorSlider are used to make the sliders controlled, so that they all update when the color is modified.

import {parseColor} from '@react-stately/color';

function Example() {
  let [color, setColor] = React.useState(parseColor('#7f007f'));
  return (
    <>
      <ColorSlider channel="red" value={color} onChange={setColor} />
      <ColorSlider channel="green" value={color} onChange={setColor} />
      <ColorSlider channel="blue" value={color} onChange={setColor} />
      <ColorSlider channel="alpha" value={color} onChange={setColor} />
    </>
  );
}
import {parseColor} from '@react-stately/color';

function Example() {
  let [color, setColor] = React.useState(
    parseColor('#7f007f')
  );
  return (
    <>
      <ColorSlider
        channel="red"
        value={color}
        onChange={setColor}
      />
      <ColorSlider
        channel="green"
        value={color}
        onChange={setColor}
      />
      <ColorSlider
        channel="blue"
        value={color}
        onChange={setColor}
      />
      <ColorSlider
        channel="alpha"
        value={color}
        onChange={setColor}
      />
    </>
  );
}
import {parseColor} from '@react-stately/color';

function Example() {
  let [
    color,
    setColor
  ] = React.useState(
    parseColor('#7f007f')
  );
  return (
    <>
      <ColorSlider
        channel="red"
        value={color}
        onChange={
          setColor
        }
      />
      <ColorSlider
        channel="green"
        value={color}
        onChange={
          setColor
        }
      />
      <ColorSlider
        channel="blue"
        value={color}
        onChange={
          setColor
        }
      />
      <ColorSlider
        channel="alpha"
        value={color}
        onChange={
          setColor
        }
      />
    </>
  );
}

HSLA#

This example shows how to build a similar color picker to the one above, using HSLA colors instead.

function Example() {
  let [color, setColor] = React.useState(parseColor('hsl(0, 100%, 50%)'));
  return (
    <>
      <ColorSlider channel="hue" value={color} onChange={setColor} />
      <ColorSlider channel="saturation" value={color} onChange={setColor} />
      <ColorSlider channel="lightness" value={color} onChange={setColor} />
      <ColorSlider channel="alpha" value={color} onChange={setColor} />
    </>
  );
}
function Example() {
  let [color, setColor] = React.useState(
    parseColor('hsl(0, 100%, 50%)')
  );
  return (
    <>
      <ColorSlider
        channel="hue"
        value={color}
        onChange={setColor}
      />
      <ColorSlider
        channel="saturation"
        value={color}
        onChange={setColor}
      />
      <ColorSlider
        channel="lightness"
        value={color}
        onChange={setColor}
      />
      <ColorSlider
        channel="alpha"
        value={color}
        onChange={setColor}
      />
    </>
  );
}
function Example() {
  let [
    color,
    setColor
  ] = React.useState(
    parseColor(
      'hsl(0, 100%, 50%)'
    )
  );
  return (
    <>
      <ColorSlider
        channel="hue"
        value={color}
        onChange={
          setColor
        }
      />
      <ColorSlider
        channel="saturation"
        value={color}
        onChange={
          setColor
        }
      />
      <ColorSlider
        channel="lightness"
        value={color}
        onChange={
          setColor
        }
      />
      <ColorSlider
        channel="alpha"
        value={color}
        onChange={
          setColor
        }
      />
    </>
  );
}

HSBA#

This example shows how to build an HSBA color picker.

function Example() {
  let [color, setColor] = React.useState(parseColor('hsb(0, 100%, 50%)'));
  return (
    <>
      <ColorSlider channel="hue" value={color} onChange={setColor} />
      <ColorSlider channel="saturation" value={color} onChange={setColor} />
      <ColorSlider channel="brightness" value={color} onChange={setColor} />
      <ColorSlider channel="alpha" value={color} onChange={setColor} />
    </>
  );
}
function Example() {
  let [color, setColor] = React.useState(
    parseColor('hsb(0, 100%, 50%)')
  );
  return (
    <>
      <ColorSlider
        channel="hue"
        value={color}
        onChange={setColor}
      />
      <ColorSlider
        channel="saturation"
        value={color}
        onChange={setColor}
      />
      <ColorSlider
        channel="brightness"
        value={color}
        onChange={setColor}
      />
      <ColorSlider
        channel="alpha"
        value={color}
        onChange={setColor}
      />
    </>
  );
}
function Example() {
  let [
    color,
    setColor
  ] = React.useState(
    parseColor(
      'hsb(0, 100%, 50%)'
    )
  );
  return (
    <>
      <ColorSlider
        channel="hue"
        value={color}
        onChange={
          setColor
        }
      />
      <ColorSlider
        channel="saturation"
        value={color}
        onChange={
          setColor
        }
      />
      <ColorSlider
        channel="brightness"
        value={color}
        onChange={
          setColor
        }
      />
      <ColorSlider
        channel="alpha"
        value={color}
        onChange={
          setColor
        }
      />
    </>
  );
}

Step#

The step prop can be used to make a ColorSlider snap its value to specific increments.

<ColorSlider channel="hue" defaultValue="hsl(0, 100%, 50%)" step={30} />
<ColorSlider
  channel="hue"
  defaultValue="hsl(0, 100%, 50%)"
  step={30}
/>
<ColorSlider
  channel="hue"
  defaultValue="hsl(0, 100%, 50%)"
  step={30}
/>

onChangeEnd#

The onChangeEnd prop can be used to handle when a user stops dragging a color slider, whereas the onChange prop is called as the user drags.

function Example() {
  let [color, setColor] = React.useState(parseColor('#7f007f'));
  return (
    <>
      <ColorSlider channel="red" defaultValue={color} onChangeEnd={setColor} />
      <p>Current color value: {color.toString('hex')}</p>
    </>
  );
}
function Example() {
  let [color, setColor] = React.useState(
    parseColor('#7f007f')
  );
  return (
    <>
      <ColorSlider
        channel="red"
        defaultValue={color}
        onChangeEnd={setColor}
      />
      <p>Current color value: {color.toString('hex')}</p>
    </>
  );
}
function Example() {
  let [
    color,
    setColor
  ] = React.useState(
    parseColor('#7f007f')
  );
  return (
    <>
      <ColorSlider
        channel="red"
        defaultValue={
          color
        }
        onChangeEnd={
          setColor
        }
      />
      <p>
        Current color
        value:{' '}
        {color.toString(
          'hex'
        )}
      </p>
    </>
  );
}

Disabled#

A ColorSlider can be disabled using the isDisabled prop. This prevents the thumb from being focused or dragged. It's up to you to style your color slider to appear disabled accordingly.

<ColorSlider channel="red" defaultValue="#7f007f" isDisabled />
<ColorSlider
  channel="red"
  defaultValue="#7f007f"
  isDisabled
/>
<ColorSlider
  channel="red"
  defaultValue="#7f007f"
  isDisabled
/>

Internationalization#


Labeling#

By default, a localized string for the channel name is used as the aria-label for the ColorSlider. When a custom aria-label or visual label is provided, it should be localized accordingly. To get a localized channel name to use as the visual label, you can use the color.getChannelName method.

Value formatting#

The aria-valuetext of the <input> element is formatted according to the user's locale automatically. If you wish to display this value visually in the <output> element, you can use the color.formatChannelValue method.

RTL#

In right-to-left languages, color sliders should be mirrored. The label should be right aligned, and the value should be left aligned. Ensure that your CSS accounts for this. Positioning of the thumb and dragging behavior is automatically mirrored by useColorSlider.