# Color slider

Picks one channel of a color, its hue, opacity, saturation or lightness, by dragging along a strip that shows the colors it moves through.

Block: `smbk-color-slider`. Status: experimental. Figma: the Figma file, https://www.figma.com/community/file/1688324308358870630. Desktop and mobile: differ by presentation only.

## Options

| Axis | Values | Default | Meaning |
|---|---|---|---|
| `channel` | `hue`, `alpha`, `saturation`, `lightness` | `hue` | Which part of the color the slider changes. Alpha is the opacity, drawn over the checkerboard and labeled “Opacity”. |
| `size` | `md`, `sm` | `md` | A 24 px strip with a 20 px thumb, or a 20 px strip with a 16 px thumb. |

Parts: `smbk-color-slider__header`, `smbk-color-slider__label`, `smbk-color-slider__value`, `smbk-color-slider__track`, `smbk-color-slider__gradient`, `smbk-color-slider__rail`, `smbk-color-slider__thumb`

## Markup

```html
<div class="smbk-color-slider smbk-color-slider--channel-hue smbk-color-slider--size-md">
  <div class="smbk-color-slider__header">
    <label
      class="smbk-label smbk-label--tone-strong smbk-label--type-default smbk-color-slider__label"
      id="react-aria-color-slider-1"
    >
      <span class="smbk-label__text">Hue</span>
    </label>
    <output
      for="react-aria-color-slider-1-0"
      aria-live="off"
      class="smbk-color-slider__value"
    >228°</output>
  </div>
  <div
    role="group"
    id="react-aria-color-slider-2"
    aria-labelledby="react-aria-color-slider-1"
    style="position:relative;touch-action:none;forced-color-adjust:none"
    class="smbk-color-slider__track"
  ><span class="smbk-color-slider__gradient" style="background:linear-gradient(to right, hsla(0, 100%, 50%, 1), hsla(60, 100%, 50%, 1), hsla(120, 100%, 50%, 1), hsla(180, 100%, 50%, 1), hsla(240, 100%, 50%, 1), hsla(300, 100%, 50%, 1), hsla(360, 100%, 50%, 1))"></span><div class="smbk-color-slider__rail"><div style="position:absolute;left:63.33333333333333%;transform:translate(-50%, -50%);touch-action:none;forced-color-adjust:none;background-color:hsla(228.21000000000004, 100%, 50%, 1)" class="smbk-color-thumb smbk-color-thumb--size-md smbk-color-slider__thumb"><input tabindex="0" id="react-aria-color-slider-1-0" aria-labelledby="react-aria-color-slider-1" type="range" min="0" max="360" step="1" aria-orientation="horizontal" aria-valuetext="228.21°, blue" aria-describedby="" aria-details="" style="border:0;clip:rect(0 0 0 0);clip-path:inset(50%);height:100%;margin:-1px;overflow:hidden;padding:0;position:absolute;width:100%;white-space:nowrap;opacity:0.0001;pointer-events:none" value="228"/></div></div></div>
</div>
```

## React

```tsx
import { ColorSlider } from './design-system/components/color-slider/color-slider.js';

<ColorSlider channel="hue" size="md" defaultValue="#3b5ce3" />
```

## Stylesheet

```css
/* smbk-color-slider — Color slider. Figma "Color slider" 7407:320 (Channel × State × Size: md, sm).
 * A label row (the label in the strong label's look at the start, the value in Body XS at the end), the small
 * stack step above a strip with fully round ends. The strip's gradient is the colors the channel moves through,
 * which change with the color, so the wrapper paints it on the gradient part as an inline background; the
 * opacity strip has the checkerboard (smbk-color-checker) under it. A hairline edge is drawn inside the strip.
 * The thumb is the color thumb, placed along the rail: the strip less half its height at each end, so the thumb
 * never passes the strip's round ends. Disabled, the strip is the disabled surface, the thumb shows no color and
 * the value turns the disabled text color.
 * A different construction from the slider: React Aria's hidden range input inside the thumb takes the keys, and
 * the strip takes the pointer. */

.smbk-color-slider {
  display: flex;
  flex-direction: column;
  gap: var(--smbk-space-stack-sm);
  width: 100%;
  min-width: 0;
}

/* ---- the label row ---- */

.smbk-color-slider__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--smbk-space-inline-md);
  min-width: 0;
}

.smbk-color-slider__value {
  margin-inline-start: auto;
  color: var(--smbk-color-text-default);
  font-family: var(--smbk-typography-body-xs-font-family);
  font-size: var(--smbk-typography-body-xs-font-size);
  font-weight: var(--smbk-typography-body-xs-font-weight);
  line-height: var(--smbk-typography-body-xs-line-height);
  letter-spacing: var(--smbk-typography-body-xs-letter-spacing);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.smbk-color-slider:has(input:disabled) .smbk-color-slider__value {
  color: var(--smbk-color-text-disabled);
}

/* ---- the strip ---- */

.smbk-color-slider__track {
  position: relative;
  box-sizing: border-box;
  width: 100%;
  height: var(--smbk-size-24);
  border-radius: var(--smbk-radius-full);
  background: var(--smbk-color-surface-disabled);
  touch-action: none;
  cursor: pointer;
}

.smbk-color-slider:has(input:disabled) .smbk-color-slider__track {
  cursor: not-allowed;
}

/* The colors, over the checkerboard on the opacity strip, with the edge drawn inside them. */
.smbk-color-slider__gradient {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: inset 0 0 0 var(--smbk-border-width-hairline) var(--smbk-color-border-edge-shade-subtle);
  forced-color-adjust: none;
}

/* Where the thumb's middle travels: the strip less half its height at each end. */
.smbk-color-slider__rail {
  position: absolute;
  inset-block: 0;
  inset-inline: calc(var(--smbk-size-24) / 2);
}

/* ---- small ---- */

.smbk-color-slider--size-sm .smbk-color-slider__track {
  height: var(--smbk-size-20);
}

.smbk-color-slider--size-sm .smbk-color-slider__rail {
  inset-inline: calc(var(--smbk-size-20) / 2);
}

/* ---- touch: the strip takes an invisible press area ----
   On a touch screen an invisible area, centred on the strip, takes the press out to the comfortable target above and
   below it, so a finger can start a drag anywhere near it; where a color area or another strip stands right above or
   below, as in the color picker, it reaches only halfway into the gap between them. The strip and its thumb stay as
   drawn. */

@media (pointer: coarse) {
  .smbk-color-slider__track::after {
    content: '';
    position: absolute;
    inset-inline: 0;
    inset-block: min(0%, (100% - var(--smbk-size-target-comfortable)) / 2);
  }

  :is(.smbk-color-area, .smbk-color-slider) + .smbk-color-slider:not(:has(.smbk-color-slider__header)) .smbk-color-slider__track::after {
    inset-block-start: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), var(--smbk-space-stack-md) / -2);
  }

  .smbk-color-slider:has(+ :is(.smbk-color-area, .smbk-color-slider:not(:has(.smbk-color-slider__header)))) .smbk-color-slider__track::after {
    inset-block-end: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), var(--smbk-space-stack-md) / -2);
  }
}

/* ---- high contrast (forced colors) ---- */
/* The strip keeps its colors, which are what it shows; its edge takes the reader's text color. */
@media (forced-colors: active) {
  .smbk-color-slider__track {
    outline: var(--smbk-border-width-hairline) solid CanvasText;
  }
}
```

## Tokens it reads

```json
{
  "space": {
    "stack": {
      "sm": {
        "$type": "dimension",
        "$value": "{{space.8}}",
        "$description": "Gap between things stacked one above another, small."
      },
      "md": {
        "$type": "dimension",
        "$value": "{{space.12}}",
        "$description": "Gap between things stacked one above another, medium."
      }
    },
    "inline": {
      "md": {
        "$type": "dimension",
        "$value": "{{space.8}}",
        "$description": "Gap between things side by side, medium."
      }
    }
  },
  "color": {
    "text": {
      "default": {
        "$type": "color",
        "$value": "{{color.grey.900}}",
        "$description": "Main text."
      },
      "disabled": {
        "$type": "color",
        "$value": "{{color.grey.500}}",
        "$description": "Text in unavailable controls."
      }
    },
    "surface": {
      "disabled": {
        "$type": "color",
        "$value": "{{color.grey.200}}",
        "$description": "Surface of a disabled card or field."
      }
    },
    "border": {
      "edge-shade-subtle": {
        "$type": "color",
        "$value": "{{color.alpha.black.10}}",
        "$description": "A faint see-through dark edge inside a colour sample (swatches, colour sliders and areas, contrast samples), so any colour stands apart from the surface behind it."
      }
    }
  },
  "typography": {
    "body-xs": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "12px",
        "fontWeight": 400,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      }
    }
  },
  "size": {
    "20": {
      "$type": "dimension",
      "$value": "20px",
      "$description": "Raw size step, 20 px. Only for fixed sizes inside a component; semantic sizes (size/control, icon, mark) point here."
    },
    "24": {
      "$type": "dimension",
      "$value": "24px",
      "$description": "Raw size step, 24 px. Only for fixed sizes inside a component; semantic sizes (size/control, icon, mark) point here."
    },
    "target": {
      "comfortable": {
        "$type": "dimension",
        "$value": "{{size.44}}",
        "$description": "Comfortable touch target (44)."
      }
    }
  },
  "radius": {
    "full": {
      "$type": "dimension",
      "$value": "{{radius.9999}}",
      "$description": "Capsule: pills, chips, switches."
    }
  },
  "border-width": {
    "hairline": {
      "$type": "dimension",
      "$value": "{{border-width.1}}",
      "$description": "Thin lines: dividers and almost every edge."
    }
  }
}
```

## Purpose

Changes one channel of a color, its hue, saturation, lightness or opacity, by dragging a thumb along a strip that shows the colors the channel moves through. Unlike the slider, which picks a number, the strip shows what each place on it looks like, and the thumb shows the color as it is now.

## Anatomy

- The header part, the top row: the label part at the start and the value part at the end, in the small body text with figures of one width. It can be left out; the label still names the slider for screen readers.
- The track part, a strip with fully round ends and a hairline edge drawn inside it.
- The gradient part, the colors the channel moves through, which the page paints as an inline background. The opacity strip has a color checker (`smbk-color-checker`) under it.
- The rail part, the stretch the thumb's middle travels: the strip less half its height at each end, so the thumb never passes the round ends.
- The thumb part, a color thumb (`smbk-color-thumb`), holding the hidden range input that takes the keys.

## When to use

- To change one part of a color, such as making a brand color lighter or a shadow more see-through.
- Beside a color area, as the color picker's panel does: the hue slider sets the hue the area shows.

## When not to use

- To pick a number. Use the [slider](slider.md).
- To enter an exact color. Use the [color field](color-field.md), which takes a code.
- To choose from a few set colors. Use the [color swatch picker](color-swatch-picker.md).

## Behavior

Pressing anywhere on the strip moves the thumb there, and dragging moves it on. While dragged, the thumb grows a step and lifts; it changes size with the state motion; under reduced motion it changes size at once and only the shadow fades. The label row shows the value: the hue in degrees, the others as percentages.

The slider holds its own color from a first color, or follows the color the page gives it. It reports the new color as the thumb moves, and again when the reader lets go. Disabled, the strip turns the disabled surface, the thumb shows no color, and the value the disabled text color.

The strip's colors and the thumb's place depend on the color, so they are set by the wrapper. In plain markup, the page paints the gradient part, places the thumb with `left` as a percentage of the rail, and paints the thumb's color, updating them as the color changes; a disabled thumb takes `data-disabled` and no color.

On a touch screen an invisible area round the strip takes a press out to the comfortable touch target above and below it, and only halfway to a color area or another strip right beside it.

## Accessibility

### What it does for you

- Each slider is a range input inside the thumb, one stop in the Tab order. Left and Right, or Up and Down, step the channel; Page Up and Page Down take larger steps; Home and End go to the ends.
- A screen reader hears the label, the value and the color in words, such as “Hue, 225°, blue”.
- The label names the slider even when the top row is hidden. The opacity slider is labeled “Opacity”.
- The thumb shows a focus ring, with the page's color in the gap, while its input has keyboard focus.
- The strip and the thumb keep their colors in forced colors mode, since the colors are what they show.

### What you must do

- Give the slider a `label` when the channel's own name, such as “Hue”, is not the name it should be read by. The channel's own name is used unless you give another.

### How to test

- Tab to the slider: one stop takes focus, and the thumb shows its ring in both themes and in a forced-color mode. Press the arrow keys, Page Up, Page Down, Home and End: the value moves and the color changes.
- With a screen reader, the slider is read by its label, its value and the color in words, such as “Hue, 225°, blue”, also when the top row is hidden.
- With the page 320 pixels wide, the strip fits and the thumb stays reachable.

### Keyboard

| Key | What it does |
|---|---|
| Tab | Moves focus to the slider. It is one stop. |
| Left Arrow, Right Arrow, Up Arrow or Down Arrow | Steps the channel. |
| Page Up or Page Down | Takes a larger step. |
| Home or End | Goes to the lowest or highest value. |

## Usage rules

### Do

- Pair the hue slider with the color area, sharing one color.
- Keep the top row when the slider stands alone, so its value can be read.

### Don't

- Hide the top row of a slider standing alone: the reader cannot see the value.
- Use it where an exact color matters without the color field beside it.
