beta

useColorWheel

Provides the behavior and accessibility implementation for a color wheel component. Color wheels allow users to adjust the hue of an HSL or HSB color value on a circular track.

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

API#


useColorWheel( props: ColorWheelAriaProps, state: ColorWheelState, inputRef: RefObject<HTMLElement> ): ColorWheelAria

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 only a hue color wheel. useColorWheel helps achieve accessible and touch-friendly color wheels that can be styled as needed.

  • Support for adjusting the hue of an HSL or HSB color value
  • Support for mouse, touch, and keyboard via the useMove hook
  • Multi-touch support
  • 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 custom step values with handling for rounding errors
  • Support for disabling the color wheel
  • 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 the localized channel name by default

Anatomy#


Color wheel anatomy diagramTrackThumb

A color wheel consists of a circular track and a thumb that the user can drag to change the color hue. A visually hidden <input> element is used to represent the value to assistive technologies.

useColorWheel returns three sets of props that you should spread onto the appropriate elements:

NameTypeDescription
trackPropsHTMLAttributes<HTMLElement>Props for the track element.
thumbPropsHTMLAttributes<HTMLElement>Props for the thumb element.
inputPropsInputHTMLAttributes<HTMLInputElement>Props for the visually hidden range input element.

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

By default, useColorWheel provides an aria-label for the localized string "Hue". If you wish to override this with a more specific label, an aria-label or aria-labelledby prop may be passed instead to identify the element to assistive technologies.

Example#


This example shows how to build a simple color wheel with a draggable thumb to adjust the hue value of a color. Styling for the track background and positioning of the thumb are provided by useColorWheel in the returned style prop for each element. The <input> element inside the thumb is used to represent the color wheel 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 {useColorWheelState} from '@react-stately/color';
import {VisuallyHidden} from '@react-aria/visually-hidden';
import {useFocusRing} from '@react-aria/focus';

const RADIUS = 100;
const TRACK_THICKNESS = 28;
const THUMB_SIZE = 20;

function ColorWheel(props) {
  let state = useColorWheelState(props);
  let inputRef = React.useRef(null);
  let {trackProps, inputProps, thumbProps} = useColorWheel(
    {
      ...props,
      outerRadius: RADIUS,
      innerRadius: RADIUS - TRACK_THICKNESS
    },
    state,
    inputRef
  );

  let {focusProps, isFocusVisible} = useFocusRing();

  return (
    <div style={{position: 'relative', display: 'inline-block'}}>
      <div {...trackProps} />
      <div
        {...thumbProps}
        style={{
          ...thumbProps.style,
          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'
        }}>
        <VisuallyHidden>
          <input {...inputProps} {...focusProps} ref={inputRef} />
        </VisuallyHidden>
      </div>
    </div>
  );
}

<ColorWheel />
import {useColorWheelState} from '@react-stately/color';
import {VisuallyHidden} from '@react-aria/visually-hidden';
import {useFocusRing} from '@react-aria/focus';

const RADIUS = 100;
const TRACK_THICKNESS = 28;
const THUMB_SIZE = 20;

function ColorWheel(props) {
  let state = useColorWheelState(props);
  let inputRef = React.useRef(null);
  let {trackProps, inputProps, thumbProps} = useColorWheel(
    {
      ...props,
      outerRadius: RADIUS,
      innerRadius: RADIUS - TRACK_THICKNESS
    },
    state,
    inputRef
  );

  let {focusProps, isFocusVisible} = useFocusRing();

  return (
    <div
      style={{
        position: 'relative',
        display: 'inline-block'
      }}>
      <div {...trackProps} />
      <div
        {...thumbProps}
        style={{
          ...thumbProps.style,
          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'
        }}>
        <VisuallyHidden>
          <input
            {...inputProps}
            {...focusProps}
            ref={inputRef}
          />
        </VisuallyHidden>
      </div>
    </div>
  );
}

<ColorWheel />
import {useColorWheelState} from '@react-stately/color';
import {VisuallyHidden} from '@react-aria/visually-hidden';
import {useFocusRing} from '@react-aria/focus';

const RADIUS = 100;
const TRACK_THICKNESS = 28;
const THUMB_SIZE = 20;

function ColorWheel(
  props
) {
  let state = useColorWheelState(
    props
  );
  let inputRef = React.useRef(
    null
  );
  let {
    trackProps,
    inputProps,
    thumbProps
  } = useColorWheel(
    {
      ...props,
      outerRadius: RADIUS,
      innerRadius:
        RADIUS -
        TRACK_THICKNESS
    },
    state,
    inputRef
  );

  let {
    focusProps,
    isFocusVisible
  } = useFocusRing();

  return (
    <div
      style={{
        position:
          'relative',
        display:
          'inline-block'
      }}>
      <div
        {...trackProps}
      />
      <div
        {...thumbProps}
        style={{
          ...thumbProps.style,
          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'
        }}>
        <VisuallyHidden>
          <input
            {...inputProps}
            {...focusProps}
            ref={
              inputRef
            }
          />
        </VisuallyHidden>
      </div>
    </div>
  );
}

<ColorWheel />

Usage#


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

Uncontrolled#

By default, ColorWheel is uncontrolled with a default value of red (hue = 0˚). You can change the default value using the defaultValue prop.

<ColorWheel defaultValue="hsl(80, 100%, 50%)" />
<ColorWheel defaultValue="hsl(80, 100%, 50%)" />
<ColorWheel defaultValue="hsl(80, 100%, 50%)" />

Controlled#

A ColorWheel can be made controlled using the value prop. The parseColor function is used to parse the initial color from an HSL string, stored in state. The onChange prop is used to update the value in state when the user drags the thumb.

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

function Example() {
  let [color, setColor] = React.useState(parseColor('hsl(0, 100%, 50%)'));
  return (
    <>
      <ColorWheel value={color} onChange={setColor} />
      <p>Current color value: {color.toString('hsl')}</p>
    </>
  );
}
import {parseColor} from '@react-stately/color';

function Example() {
  let [color, setColor] = React.useState(
    parseColor('hsl(0, 100%, 50%)')
  );
  return (
    <>
      <ColorWheel value={color} onChange={setColor} />
      <p>Current color value: {color.toString('hsl')}</p>
    </>
  );
}
import {parseColor} from '@react-stately/color';

function Example() {
  let [
    color,
    setColor
  ] = React.useState(
    parseColor(
      'hsl(0, 100%, 50%)'
    )
  );
  return (
    <>
      <ColorWheel
        value={color}
        onChange={
          setColor
        }
      />
      <p>
        Current color
        value:{' '}
        {color.toString(
          'hsl'
        )}
      </p>
    </>
  );
}

onChangeEnd#

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

function Example() {
  let [color, setColor] = React.useState(parseColor('hsl(0, 100%, 50%)'));
  return (
    <>
      <ColorWheel defaultValue={color} onChangeEnd={setColor} />
      <p>Current color value: {color.toString('hsl')}</p>
    </>
  );
}
function Example() {
  let [color, setColor] = React.useState(
    parseColor('hsl(0, 100%, 50%)')
  );
  return (
    <>
      <ColorWheel
        defaultValue={color}
        onChangeEnd={setColor}
      />
      <p>Current color value: {color.toString('hsl')}</p>
    </>
  );
}
function Example() {
  let [
    color,
    setColor
  ] = React.useState(
    parseColor(
      'hsl(0, 100%, 50%)'
    )
  );
  return (
    <>
      <ColorWheel
        defaultValue={
          color
        }
        onChangeEnd={
          setColor
        }
      />
      <p>
        Current color
        value:{' '}
        {color.toString(
          'hsl'
        )}
      </p>
    </>
  );
}

Step#

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

<ColorWheel step={30} />
<ColorWheel step={30} />
<ColorWheel
  step={30}
/>

Disabled#

A ColorWheel 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 wheel to appear disabled accordingly.

<ColorWheel defaultValue="hsl(80, 100%, 50%)" isDisabled />
<ColorWheel
  defaultValue="hsl(80, 100%, 50%)"
  isDisabled
/>
<ColorWheel
  defaultValue="hsl(80, 100%, 50%)"
  isDisabled
/>

Internationalization#


Labeling#

By default, a localized string for the "hue" channel name is used as the aria-label for the ColorWheel. When a custom aria-label is provided, it should be localized accordingly. To get a localized channel name to use as a 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, you can use the color.formatChannelValue method.

RTL#

Color wheels should not be mirrored in right-to-left languages.