Skip to content

ColorSlider ​

A 1D slider component for adjusting a single color channel, with a gradient track that reflects the current color.

Preview ​

Source code
vue
<script setup lang="ts">
import {
  ColorSliderRoot,
  ColorSliderTrack,
  ColorSliderGradient,
  ColorSliderThumb,
  useColor,
} from "@urcolor/vue";

const { color } = useColor("hsl(210, 80%, 50%)");
</script>

<template>
  <ColorSliderRoot
    v-model="color"
    color-space="hsl"
    channel="h"
    as="div"
    class="w-full"
  >
    <ColorSliderTrack
      as="div"
      class="relative h-5 overflow-hidden rounded-xl"
    >
      <ColorSliderGradient
        as="div"
        class="absolute inset-0 rounded-xl"
        :colors="['red', 'yellow', 'lime', 'cyan', 'blue', 'magenta', 'red']"
      />
      <ColorSliderThumb
        class="
          block size-5 rounded-full border-[2.5px] border-white bg-white
          shadow-[0_0_0_1px_rgba(0,0,0,0.3),0_2px_4px_rgba(0,0,0,0.3)]
          focus-visible:shadow-[0_0_0_1px_rgba(0,0,0,0.3),0_0_0_3px_rgba(66,153,225,0.6)]
        "
        aria-label="Hue"
      />
    </ColorSliderTrack>
  </ColorSliderRoot>
</template>

Anatomy ​

vue
<template>
  <ColorSliderRoot>
    <ColorSliderTrack>
      <ColorSliderGradient />
      <ColorSliderRange />
      <ColorSliderThumb />
    </ColorSliderTrack>
  </ColorSliderRoot>
</template>

ColorSliderRange is optional. Add it only when you want a filled portion of the track.

TIP

The Vue package has no Control part. React needs one because Base UI handles pointer interaction on Slider.Control; Reka UI's slider does not, and Svelte and Angular ship an optional Control that is a pure styling hook.

Examples ​

Hue ​

Source code
vue
<script setup lang="ts">
import {
  ColorSliderRoot,
  ColorSliderTrack,
  ColorSliderGradient,
  ColorSliderThumb,
  useColor,
} from "@urcolor/vue";

const { color } = useColor("hsl(210, 80%, 50%)");
</script>

<template>
  <ColorSliderRoot
    v-model="color"
    color-space="hsl"
    channel="h"
    as="div"
    class="w-full"
  >
    <ColorSliderTrack
      as="div"
      class="relative h-5 overflow-hidden rounded-xl"
    >
      <ColorSliderGradient
        as="div"
        class="absolute inset-0 rounded-xl"
        :colors="['red', 'yellow', 'lime', 'cyan', 'blue', 'magenta', 'red']"
      />
      <ColorSliderThumb
        class="
          block size-5 rounded-full border-[2.5px] border-white bg-white
          shadow-[0_0_0_1px_rgba(0,0,0,0.3),0_2px_4px_rgba(0,0,0,0.3)]
          focus-visible:shadow-[0_0_0_1px_rgba(0,0,0,0.3),0_0_0_3px_rgba(66,153,225,0.6)]
        "
        aria-label="Hue"
      />
    </ColorSliderTrack>
  </ColorSliderRoot>
</template>

Saturation ​

Source code
vue
<script setup lang="ts">
import { computed } from "vue";
import { getChannelConfig } from "@urcolor/shared";
import {
  ColorSliderRoot,
  ColorSliderTrack,
  ColorSliderGradient,
  ColorSliderThumb,
  useColor,
} from "@urcolor/vue";

const { color } = useColor("hsl(210, 80%, 50%)");

const gradientColors = computed(() => {
  if (!color.value) return ["gray", "blue"];
  const cfg = getChannelConfig("hsl", "s");
  if (!cfg) return ["gray", "blue"];
  const steps = 7;
  const colors: string[] = [];
  const cMin = cfg.nativeMin ?? cfg.min;
  const cMax = cfg.nativeMax ?? cfg.max;
  for (let i = 0; i < steps; i++) {
    const t = i / (steps - 1);
    const val = cMin + t * (cMax - cMin);
    colors.push(color.value.with({ space: "hsl", s: val }).toString());
  }
  return colors;
});
</script>

<template>
  <ColorSliderRoot
    v-model="color"
    color-space="hsl"
    channel="s"
    as="div"
    class="w-full"
  >
    <ColorSliderTrack
      as="div"
      class="relative h-5 overflow-hidden rounded-xl"
    >
      <ColorSliderGradient
        as="div"
        class="absolute inset-0 rounded-xl"
        :colors="gradientColors"
      />
      <ColorSliderThumb
        class="
          block size-5 rounded-full border-[2.5px] border-white bg-white
          shadow-[0_0_0_1px_rgba(0,0,0,0.3),0_2px_4px_rgba(0,0,0,0.3)]
          focus-visible:shadow-[0_0_0_1px_rgba(0,0,0,0.3),0_0_0_3px_rgba(66,153,225,0.6)]
        "
        aria-label="Saturation"
      />
    </ColorSliderTrack>
  </ColorSliderRoot>
</template>

Lightness ​

Source code
vue
<script setup lang="ts">
import { computed } from "vue";
import { getChannelConfig } from "@urcolor/shared";
import {
  ColorSliderRoot,
  ColorSliderTrack,
  ColorSliderGradient,
  ColorSliderThumb,
  useColor,
} from "@urcolor/vue";

const { color } = useColor("hsl(210, 80%, 50%)");

const gradientColors = computed(() => {
  if (!color.value) return ["black", "white"];
  const cfg = getChannelConfig("hsl", "l");
  if (!cfg) return ["black", "white"];
  const steps = 7;
  const colors: string[] = [];
  const cMin = cfg.nativeMin ?? cfg.min;
  const cMax = cfg.nativeMax ?? cfg.max;
  for (let i = 0; i < steps; i++) {
    const t = i / (steps - 1);
    const val = cMin + t * (cMax - cMin);
    colors.push(color.value.with({ space: "hsl", l: val }).toString());
  }
  return colors;
});
</script>

<template>
  <ColorSliderRoot
    v-model="color"
    color-space="hsl"
    channel="l"
    as="div"
    class="w-full"
  >
    <ColorSliderTrack
      as="div"
      class="relative h-5 overflow-hidden rounded-xl"
    >
      <ColorSliderGradient
        as="div"
        class="absolute inset-0 rounded-xl"
        :colors="gradientColors"
      />
      <ColorSliderThumb
        class="
          block size-5 rounded-full border-[2.5px] border-white bg-white
          shadow-[0_0_0_1px_rgba(0,0,0,0.3),0_2px_4px_rgba(0,0,0,0.3)]
          focus-visible:shadow-[0_0_0_1px_rgba(0,0,0,0.3),0_0_0_3px_rgba(66,153,225,0.6)]
        "
        aria-label="Lightness"
      />
    </ColorSliderTrack>
  </ColorSliderRoot>
</template>

Vertical ​

Source code
vue
<script setup lang="ts">
import {
  ColorSliderRoot,
  ColorSliderTrack,
  ColorSliderGradient,
  ColorSliderThumb,
  useColor,
} from "@urcolor/vue";

const { color } = useColor("hsl(210, 80%, 50%)");
</script>

<template>
  <ColorSliderRoot
    v-model="color"
    color-space="hsl"
    channel="h"
    orientation="vertical"
    as="div"
    class="h-[150px] w-auto"
  >
    <ColorSliderTrack
      as="div"
      class="relative h-full w-5 overflow-hidden rounded-xl"
    >
      <ColorSliderGradient
        as="div"
        class="absolute inset-0 rounded-xl"
        :colors="['red', 'yellow', 'lime', 'cyan', 'blue', 'magenta', 'red']"
      />
      <ColorSliderThumb
        class="
          block size-5 rounded-full border-[2.5px] border-white bg-white
          shadow-[0_0_0_1px_rgba(0,0,0,0.3),0_2px_4px_rgba(0,0,0,0.3)]
          focus-visible:shadow-[0_0_0_1px_rgba(0,0,0,0.3),0_0_0_3px_rgba(66,153,225,0.6)]
        "
        aria-label="Hue (vertical)"
      />
    </ColorSliderTrack>
  </ColorSliderRoot>
</template>

With Alpha ​

Pass :channel-overrides="false" on ColorSliderGradient to reflect the color's alpha as opacity on the gradient. ColorSliderGradient paints the checkerboard behind the canvas automatically, so transparency is visible with no extra element.

vue
<template>
  <ColorSliderRoot
    :model-value="color"
    color-space="hsl"
    channel="h"
    @update:model-value="onColorUpdate"
  >
    <ColorSliderTrack>
      <ColorSliderGradient
        :colors="['red', 'yellow', 'lime', 'cyan', 'blue', 'magenta', 'red']"
        :channel-overrides="false"
      />
      <ColorSliderThumb />
    </ColorSliderTrack>
  </ColorSliderRoot>
</template>

Alpha Channel ​

Set channel="alpha" to create an opacity slider. The gradient automatically renders with transparency.

vue
<template>
  <ColorSliderRoot
    :model-value="color"
    color-space="hsl"
    channel="alpha"
    @update:model-value="onColorUpdate"
  >
    <ColorSliderTrack>
      <ColorSliderGradient :colors="['hsla(210, 80%, 50%, 0)', 'hsl(210, 80%, 50%)']" />
      <ColorSliderThumb />
    </ColorSliderTrack>
  </ColorSliderRoot>
</template>

API Reference ​

The root's context is readable with injectColorSliderRootContext().

ColorSliderRoot ​

The root container that manages slider state and color channel binding. Renders Reka UI's SliderRoot.

PropTypeDefaultDescription
modelValueColor | string | null—Controlled color value (v-model).
defaultValueColor | string'hsl(0, 100%, 50%)'Initial color when uncontrolled.
colorSpaceSpaceId'hsl'Color space (e.g. 'hsl', 'oklch').
channelstring'h'Channel to control (e.g. 'h', 's', 'l', 'alpha').
stepnumberAutoStepping interval. Derived from the channel config when omitted.
disabledbooleanfalseDisables interaction.
dir'ltr' | 'rtl'—Reading direction.
invertedbooleanfalseVisually invert the slider.
orientation'horizontal' | 'vertical''horizontal'Slider orientation.
namestring—Hidden input name for form submission.
requiredbooleanfalseMarks as required for form submission.
asstring'span'The element or component to render as.
asChildbooleanfalseMerge props onto the single child instead of rendering an element.
EventPayloadDescription
update:modelValueColor | undefinedEmitted whenever the color changes.
update:colorColorMirrors update:modelValue; present for API parity.
changeColorEmitted on every value change, including mid-drag.
changeEndColorEmitted when a change-producing interaction ends.

The default slot receives { modelValue }, the current Color.

ColorSliderTrack ​

The track area that contains the gradient and thumb. Renders Reka UI's SliderTrack, a <span>, and extends its SliderTrackProps.

ColorSliderGradient ​

Renders the slider's color ramp inside a wrapper element. A one-dimensional ramp has an exact CSS equivalent in every color space, so by default this paints a linear-gradient and renders no <canvas> at all, it appears in server-rendered HTML and costs no WebGL context. The transparency checkerboard is the wrapper's own CSS background, which the gradient composites over, so no separate part is needed for it.

Setting interpolationSpace does not change that: the stops are computed in the requested space and emitted densely, so the CSS path stays exact.

PropTypeDefaultDescription
renderer'auto' | 'css' | 'canvas''auto'Which painter to use. 'auto' paints with stacked CSS gradients when an exact recipe exists for the color space and channels, and falls back to the canvas otherwise. 'css' forces the CSS path and warns in development if no recipe exists. 'canvas' always paints into a <canvas>.
colorsstring[]AutoArray of color stops. Computed from the slider's channel and current color when omitted: 36 stops across a cyclic channel such as hue, 16 otherwise, or 12 on the canvas path, which the shader's uniform slots cap. At least two valid stops are required, or nothing is painted.
anglenumberAutoGradient rotation in degrees (0 = left-to-right, 90 = top-to-bottom). Normalized to 0–360; defaults to 90 when the slider is vertical, 0 otherwise.
interpolationSpaceSpaceId—Color space for perceptual interpolation (e.g. 'oklch').
channelOverridesRecord<string, number> | false{ alpha: 1 }Lock specific channels to fixed values in the gradient. Set to false to reflect all channels from current color including alpha. E.g. { s: 1, v: 1, alpha: 1 } for an immutable hue gradient in HSV.
asstring'span'The element or component to render as.
asChildbooleanfalseMerge props onto the single child instead of rendering an element.

ColorSliderCheckerboard deprecated ​

Deprecated

ColorSliderGradient now paints the checkerboard itself, so this component is no longer needed and is kept only for backwards compatibility. It emits a one-time console warning in development. To render a checkerboard elsewhere, apply a CSS repeating-conic-gradient background to your own element.

Renders a checkerboard pattern behind the gradient to visualize alpha transparency. Place it inside ColorSliderTrack before ColorSliderGradient.

ColorSliderThumb ​

The draggable handle, and the slider's only focusable element. Renders Reka UI's SliderThumb, which supplies role="slider", the value ARIA and tabindex. On top of that this part sets aria-label from the channel's label and aria-valuetext from the formatted channel value.

Extends Reka UI's SliderThumbProps.

PropTypeDefaultDescription
aria-labelstringChannel labelOverrides the generated label, e.g. "Hue". Passed as a fallthrough attribute.
asstring'span'The element or component to render as.
asChildbooleanfalseMerge props onto the single child instead of rendering an element.

The default slot receives { channelName, channelValue }.

ColorSliderRange ​

The filled range portion of the track. Renders Reka UI's SliderRange and extends its SliderRangeProps.

Data Attributes ​

Reka UI applies these to the parts it renders.

AttributePartPresent when
data-orientationRoot, Track, Range, ThumbAlways; the value is horizontal or vertical.
data-disabledRoot, Track, Range, ThumbThe root is disabled.

ColorSliderGradient and ColorSliderCheckerboard are plain elements and carry no state attributes; style them from an ancestor's.

CSS Variables ​

The transparency grid reads three custom properties and no component writes them, so a rule anywhere above the element wins:

VariableDefaultDescription
--urcolor-checkerboard-darkrgb(230, 230, 230)The darker of the two checks.
--urcolor-checkerboard-lightwhiteThe lighter of the two checks.
--urcolor-checkerboard-size16pxThe tile size, applied to both axes.

The grid is a single background shorthand, so an invalid value invalidates the whole declaration rather than its own layer. Keep overrides to a <color> and a <length>.

Accessibility ​

ColorSlider provides a standard slider interface built on top of Reka UI's slider primitives, ensuring robust screen reader support.

ARIA Labels ​

AttributeDescription
role="slider"Applied to ColorSliderThumb for screen reader recognition.
aria-labelDefaults to the channel's label, e.g. "Hue" or "Alpha". Pass your own aria-label on the thumb to override.
aria-valuemin / aria-valuemaxThe channel's range in display units.
aria-valuenowThe current channel value in display units.
aria-valuetextThe value formatted with its unit, e.g. "210°", "80%".
aria-orientationReflects the root's orientation.

Keyboard Navigation ​

KeyAction
Arrow Right / Arrow UpIncrease by one step
Arrow Left / Arrow DownDecrease by one step
Shift + ArrowMove by 10 steps
Page Up / Page DownIncrease / decrease by 10 steps
HomeMove to the channel minimum
EndMove to the channel maximum

changeEnd fires once when a change-producing interaction ends, not on every key repeat.