Skip to content

Slider

Experimental Alpha

This library is a work-in-progress. We are releasing it early to gather feedback, but it is not ready for production.

Slider allows a user to select a value or range of values by moving a thumb along a track. It's ideal for adjusting settings like volume, brightness, price ranges, and other numeric inputs.

Examples

Vue
Lit
React
Live Preview

Basic Slider

Single value slider with label

Dual Range Slider

Select a range with two thumbs

Size Variants

Three size options: small, default, and large

Style Variants

Choose from filled and monochrome variants

Default (Primary Color)
Filled Variant
Monochrome (Adapts to Dark Mode)
Monochrome Filled

With Ticks and Tooltip

Display tick marks and value tooltip

Step Increments

Control the granularity of value changes. Tick marks show the step intervals.

Custom Range

Configure min, max, and step values

Vertical Orientation

Vertical sliders for compact layouts

States

Disabled, readonly, and invalid states

With Help Text

Provide additional guidance with help text

Event Testing

Demonstrates the dual-dispatch event pattern: both CustomEvents (via @input/@change) and native events work simultaneously. Drag the slider to see real-time events.

Event Log (last 10 events):

Interact with the slider to see events...
Current value: 50

Dual Range Events

Event handling with dual range sliders

Range Event Log (last 10 events):

Drag the range handles to see events...
Current range: $200 - $800

CSS Shadow Parts Customization

Use CSS Shadow Parts to customize the component's appearance. These examples show customized track, thumb, and label styling.

External Label Support

The Slider component supports external labels with helper text, required fields, and validation states.

Label Positioning

Control label position with label-position: 'top' (default), 'start', 'end', or 'bottom'.

View Vue Code
<template>
  <section>
    <div class="mbe4">
      <h2>Basic Slider</h2>
      <p class="mbs2 mbe3">Single value slider with label</p>
    </div>
    <div class="mbe4">
      <VueSlider
        label="Volume"
        :value="75"
      />
    </div>

    <div class="mbe4">
      <h2>Dual Range Slider</h2>
      <p class="mbs2 mbe3">Select a range with two thumbs</p>
    </div>
    <div class="mbe4">
      <VueSlider
        label="Price Range ($0 - $1000)"
        dual
        :min="0"
        :max="1000"
        :value="[200, 800]"
      />
    </div>

    <div class="mbe4">
      <h2>Size Variants</h2>
      <p class="mbs2 mbe3">Three size options: small, default, and large</p>
    </div>
    <div
      class="mbe4"
      style="display: flex; flex-direction: column; gap: 2rem;"
    >
      <VueSlider
        label="Small"
        size="small"
        :value="30"
      />
      <VueSlider
        label="Default"
        size="default"
        :value="50"
      />
      <VueSlider
        label="Large"
        size="large"
        :value="70"
      />
    </div>

    <div class="mbe4">
      <h2>Style Variants</h2>
      <p class="mbs2 mbe3">Choose from filled and monochrome variants</p>
    </div>
    <div
      class="mbe4"
      style="display: flex; flex-direction: column; gap: 2rem;"
    >
      <fieldset style="border: 1px solid var(--ag-border); padding: 1rem; border-radius: var(--ag-radius-md);">
        <legend style="padding: 0 0.5rem; font-weight: 600;">Default (Primary Color)</legend>
        <VueSlider
          label="Standard"
          :value="50"
        />
      </fieldset>

      <fieldset style="border: 1px solid var(--ag-border); padding: 1rem; border-radius: var(--ag-radius-md);">
        <legend style="padding: 0 0.5rem; font-weight: 600;">Filled Variant</legend>
        <VueSlider
          label="Filled Thumb"
          filled
          :value="60"
        />
      </fieldset>

      <fieldset style="border: 1px solid var(--ag-border); padding: 1rem; border-radius: var(--ag-radius-md);">
        <legend style="padding: 0 0.5rem; font-weight: 600;">Monochrome (Adapts to Dark Mode)</legend>
        <VueSlider
          label="Monochrome"
          monochrome
          :value="70"
        />
      </fieldset>

      <fieldset style="border: 1px solid var(--ag-border); padding: 1rem; border-radius: var(--ag-radius-md);">
        <legend style="padding: 0 0.5rem; font-weight: 600;">Monochrome Filled</legend>
        <VueSlider
          label="Monochrome Filled"
          monochrome
          filled
          :value="80"
        />
      </fieldset>
    </div>

    <div class="mbe4">
      <h2>With Ticks and Tooltip</h2>
      <p class="mbs2 mbe3">Display tick marks and value tooltip</p>
    </div>
    <div
      class="mbe4"
      style="display: flex; flex-direction: column; gap: 2rem;"
    >
      <VueSlider
        label="With Tick Marks"
        show-ticks
        :tick-step="25"
        :value="50"
      />
      <VueSlider
        label="With Tooltip"
        show-tooltip
        :value="65"
      />
      <VueSlider
        label="Ticks + Tooltip"
        show-ticks
        show-tooltip
        :tick-step="20"
        :value="40"
      />
    </div>

    <div class="mbe4">
      <h2>Step Increments</h2>
      <p class="mbs2 mbe3">Control the granularity of value changes. Tick marks show the step intervals.</p>
    </div>
    <div
      class="mbe4"
      style="display: flex; flex-direction: column; gap: 2rem;"
    >
      <VueSlider
        label="Step 5"
        :step="5"
        :value="50"
        show-ticks
        :tick-step="5"
      />
      <VueSlider
        label="Step 10"
        :step="10"
        :value="50"
        show-ticks
        :tick-step="10"
      />
      <VueSlider
        label="Step 25 (Coarse)"
        :step="25"
        :value="50"
        show-ticks
        :tick-step="25"
      />
    </div>

    <div class="mbe4">
      <h2>Custom Range</h2>
      <p class="mbs2 mbe3">Configure min, max, and step values</p>
    </div>
    <div
      class="mbe4"
      style="display: flex; flex-direction: column; gap: 2rem;"
    >
      <VueSlider
        label="Temperature (°C)"
        :min="-20"
        :max="40"
        :step="5"
        :value="20"
      />
      <VueSlider
        label="Percentage"
        :min="0"
        :max="100"
        :step="1"
        :value="75"
        help-text="Adjust from 0% to 100%"
      />
    </div>

    <div class="mbe4">
      <h2>Vertical Orientation</h2>
      <p class="mbs2 mbe3">Vertical sliders for compact layouts</p>
    </div>
    <div
      class="mbe4"
      style="display: flex; gap: 2rem; align-items: flex-end;"
    >
      <VueSlider
        label="Volume"
        vertical
        :value="75"
      />
      <VueSlider
        label="Bass"
        vertical
        size="small"
        :value="50"
      />
      <VueSlider
        label="Treble"
        vertical
        size="large"
        :value="60"
      />
      <VueSlider
        label="Balance"
        vertical
        dual
        :value="[25, 75]"
      />
    </div>

    <div class="mbe4">
      <h2>States</h2>
      <p class="mbs2 mbe3">Disabled, readonly, and invalid states</p>
    </div>
    <div
      class="mbe4"
      style="display: flex; flex-direction: column; gap: 2rem;"
    >
      <VueSlider
        label="Disabled"
        disabled
        :value="50"
        help-text="This slider is disabled"
      />
      <VueSlider
        label="Readonly"
        readonly
        :value="65"
        help-text="This slider is readonly"
      />
      <VueSlider
        label="Invalid"
        invalid
        error-message="Value must be at least 50"
        :value="30"
      />
    </div>

    <div class="mbe4">
      <h2>With Help Text</h2>
      <p class="mbs2 mbe3">Provide additional guidance with help text</p>
    </div>
    <div class="mbe4">
      <VueSlider
        label="Brightness"
        help-text="Adjust screen brightness level (affects battery life)"
        :value="75"
      />
    </div>

    <div class="mbe4">
      <h2>Event Testing</h2>
      <p class="mbs2 mbe3">
        Demonstrates the dual-dispatch event pattern: both CustomEvents (via @input/@change)
        and native events work simultaneously. Drag the slider to see real-time events.
      </p>
    </div>
    <div class="mbe4">
      <VueSlider
        label="Interactive Slider"
        :value="eventValue"
        @input="handleInput"
        @change="handleChange"
        @focus="handleFocus"
        @blur="handleBlur"
      />
      <div
        class="event-log"
        style="margin-top: 1.5rem; padding: 1rem; background: var(--ag-background-secondary); border-radius: var(--ag-radius-md); border: 1px solid var(--ag-border);"
      >
        <h3 style="margin-top: 0; margin-bottom: 0.75rem; font-size: 0.95rem;">Event Log (last 10 events):</h3>
        <div
          v-if="events.length === 0"
          style="color: var(--ag-text-muted); font-size: 0.875rem; font-style: italic;"
        >
          Interact with the slider to see events...
        </div>
        <div
          v-for="(event, i) in events"
          :key="i"
          style="font-family: monospace; font-size: 0.875rem; padding: 0.25rem 0; color: var(--ag-text-primary);"
        >
          {{ event }}
        </div>
      </div>
      <div style="margin-top: 1rem; font-size: 0.875rem; color: var(--ag-text-secondary);">
        Current value: <strong>{{ eventValue }}</strong>
      </div>
    </div>

    <div class="mbe4">
      <h2>Dual Range Events</h2>
      <p class="mbs2 mbe3">Event handling with dual range sliders</p>
    </div>
    <div class="mbe4">
      <VueSlider
        label="Price Range Filter"
        dual
        :min="0"
        :max="1000"
        :step="10"
        :value="rangeValue"
        @input="handleRangeInput"
        @change="handleRangeChange"
      />
      <div
        class="event-log"
        style="margin-top: 1.5rem; padding: 1rem; background: var(--ag-background-secondary); border-radius: var(--ag-radius-md); border: 1px solid var(--ag-border);"
      >
        <h3 style="margin-top: 0; margin-bottom: 0.75rem; font-size: 0.95rem;">Range Event Log (last 10 events):</h3>
        <div
          v-if="rangeEvents.length === 0"
          style="color: var(--ag-text-muted); font-size: 0.875rem; font-style: italic;"
        >
          Drag the range handles to see events...
        </div>
        <div
          v-for="(event, i) in rangeEvents"
          :key="i"
          style="font-family: monospace; font-size: 0.875rem; padding: 0.25rem 0; color: var(--ag-text-primary);"
        >
          {{ event }}
        </div>
      </div>
      <div style="margin-top: 1rem; font-size: 0.875rem; color: var(--ag-text-secondary);">
        Current range: <strong>${{ rangeValue[0] }} - ${{ rangeValue[1] }}</strong>
      </div>
    </div>

    <div class="mbe4">
      <h2>CSS Shadow Parts Customization</h2>
      <p class="mbs2 mbe3">
        Use CSS Shadow Parts to customize the component's appearance.
        These examples show customized track, thumb, and label styling.
      </p>
    </div>
    <div
      class="mbe4"
      style="display: flex; flex-direction: column; gap: 2rem;"
    >
      <VueSlider
        label="Gradient Track"
        :value="60"
        class="custom-slider-1"
      />
      <VueSlider
        label="Large Thumb with Shadow"
        :value="70"
        class="custom-slider-2"
      />
      <VueSlider
        label="Fully Customized"
        :value="50"
        class="custom-slider-3"
        help-text="Custom colors, sizing, and typography"
      />
    </div>

    <div class="mbe4">
      <h2>External Label Support</h2>
      <p class="mbs2 mbe3">
        The Slider component supports external labels with helper text, required fields, and validation states.
      </p>
    </div>
    <div
      class="mbe4"
      style="max-width: 600px;"
    >
      <div class="mbe3">
        <VueSlider
          label="Volume Level"
          :min="0"
          :max="100"
          :value="75"
        />
      </div>

      <div class="mbe3">
        <VueSlider
          label="Brightness"
          help-text="Adjust screen brightness level (affects battery life)"
          :min="0"
          :max="100"
          :value="50"
        />
      </div>

      <div class="mbe3">
        <VueSlider
          label="Temperature"
          :required="true"
          help-text="This field is required"
          :min="-20"
          :max="40"
          :value="20"
        />
      </div>

      <div class="mbe3">
        <VueSlider
          label="Price Range"
          :required="true"
          :invalid="true"
          error-message="Please select a valid price range"
          :min="0"
          :max="1000"
          :value="50"
        />
      </div>
    </div>

    <div class="mbe4">
      <h2>Label Positioning</h2>
      <p class="mbs2 mbe3">
        Control label position with <code>label-position</code>: 'top' (default), 'start', 'end', or 'bottom'.
      </p>
    </div>
    <div
      class="mbe4"
      style="max-width: 600px;"
    >
      <div class="mbe3">
        <VueSlider
          label="Top Label (Default)"
          label-position="top"
          :min="0"
          :max="100"
          :value="50"
        />
      </div>

      <div class="mbe3">
        <VueSlider
          label="Start Position"
          label-position="start"
          :min="0"
          :max="100"
          :value="60"
        />
      </div>

      <div class="mbe3">
        <VueSlider
          label="End Position"
          label-position="end"
          :min="0"
          :max="100"
          :value="70"
        />
      </div>

      <div class="mbe3">
        <VueSlider
          label="Bottom Position"
          label-position="bottom"
          help-text="Bottom label position for alternative layouts"
          :min="0"
          :max="100"
          :value="40"
        />
      </div>
    </div>
  </section>
</template>

<script lang="ts">
import { defineComponent, ref } from "vue";
import { VueSlider } from "agnosticui-core/slider/vue";

export default defineComponent({
  name: "SliderExamples",
  components: {
    VueSlider,
  },
  setup() {
    const eventValue = ref(50);
    const events = ref<string[]>([]);

    const rangeValue = ref<[number, number]>([200, 800]);
    const rangeEvents = ref<string[]>([]);

    const addEvent = (event: string) => {
      events.value = [
        ...events.value,
        `${new Date().toLocaleTimeString()}: ${event}`,
      ].slice(-10);
    };

    const addRangeEvent = (event: string) => {
      rangeEvents.value = [
        ...rangeEvents.value,
        `${new Date().toLocaleTimeString()}: ${event}`,
      ].slice(-10);
    };

    const handleInput = (detail: { value: number | [number, number] }) => {
      eventValue.value = detail.value as number;
      addEvent(`📝 input: value=${detail.value}`);
    };

    const handleChange = (detail: { value: number | [number, number] }) => {
      addEvent(`✅ change: value=${detail.value}`);
    };

    const handleFocus = () => {
      addEvent("🎯 focus");
    };

    const handleBlur = () => {
      addEvent("👋 blur");
    };

    const handleRangeInput = (detail: { value: number | [number, number] }) => {
      rangeValue.value = detail.value as [number, number];
      const [min, max] = detail.value as [number, number];
      addRangeEvent(`📝 input: range=[$${min}, $${max}]`);
    };

    const handleRangeChange = (detail: {
      value: number | [number, number];
    }) => {
      const [min, max] = detail.value as [number, number];
      addRangeEvent(`✅ change: range=[$${min}, $${max}]`);
    };

    return {
      eventValue,
      events,
      rangeValue,
      rangeEvents,
      handleInput,
      handleChange,
      handleFocus,
      handleBlur,
      handleRangeInput,
      handleRangeChange,
    };
  },
});
</script>

<style>
/* CSS Shadow Parts customization examples */

/* Custom slider 1: Gradient track */
.custom-slider-1::part(ag-slider-track) {
  height: 10px;
  background: var(--ag-background-tertiary);
}

.custom-slider-1::part(ag-slider-progress) {
  background: linear-gradient(90deg, var(--ag-green-500), var(--ag-blue-500));
}

/* Custom slider 2: Large thumb with enhanced shadow */
.custom-slider-2::part(ag-slider-thumb) {
  width: 28px;
  height: 28px;
  border: 4px solid var(--ag-primary);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}

.custom-slider-2::part(ag-slider-track) {
  height: 8px;
}

/* Custom slider 3: Fully customized */
.custom-slider-3::part(ag-slider-label) {
  font-weight: 700;
  font-size: 1.1rem;
}

.custom-slider-3::part(ag-slider-track) {
  height: 12px;
  background: var(--ag-background-tertiary);
  border-radius: var(--ag-radius-lg);
  border: 2px solid var(--ag-border);
}

.custom-slider-3::part(ag-slider-progress) {
  background: linear-gradient(
    135deg,
    var(--ag-purple-500) 0%,
    var(--ag-blue-500) 100%
  );
  border-radius: var(--ag-radius-lg);
}

.custom-slider-3::part(ag-slider-thumb) {
  width: 32px;
  height: 32px;
  background: var(--ag-white);
  border: 4px solid var(--ag-purple-500);
  box-shadow: 0 4px 16px rgba(137, 87, 229, 0.4);
}

.custom-slider-3::part(ag-slider-help-text) {
  color: var(--ag-purple-500);
  font-style: italic;
}
</style>
Live Preview
View Lit / Web Component Code
import { LitElement, html } from 'lit';
import 'agnosticui-core/slider';

export class SliderLitExamples extends LitElement {
  static properties = {
    eventValue: { type: Number },
    events: { type: Array },
    rangeValue: { type: Array },
    rangeEvents: { type: Array },
  };

  constructor() {
    super();
    this.eventValue = 50;
    this.events = [];
    this.rangeValue = [200, 800];
    this.rangeEvents = [];
  }

  // CRITICAL: Must include createRenderRoot() to use light DOM
  // Without this, global CSS utility classes won't work!
  createRenderRoot() {
    return this;
  }

  addEvent(event) {
    this.events = [
      ...this.events,
      `${new Date().toLocaleTimeString()}: ${event}`,
    ].slice(-10);
  }

  addRangeEvent(event) {
    this.rangeEvents = [
      ...this.rangeEvents,
      `${new Date().toLocaleTimeString()}: ${event}`,
    ].slice(-10);
  }

  handleInput(e) {
    this.eventValue = e.detail.value;
    this.addEvent(`📝 input: value=${e.detail.value}`);
  }

  handleChange(e) {
    this.addEvent(`✅ change: value=${e.detail.value}`);
  }

  handleFocus() {
    this.addEvent('🎯 focus');
  }

  handleBlur() {
    this.addEvent('👋 blur');
  }

  handleRangeInput(e) {
    this.rangeValue = e.detail.value;
    const [min, max] = e.detail.value;
    this.addRangeEvent(`📝 input: range=[$${min}, $${max}]`);
  }

  handleRangeChange(e) {
    const [min, max] = e.detail.value;
    this.addRangeEvent(`✅ change: range=[$${min}, $${max}]`);
  }

  render() {
    return html`
      <section>
        <div class="mbe4">
          <h2>Basic Slider</h2>
          <p class="mbs2 mbe3">Single value slider with label</p>
        </div>
        <div class="mbe4">
          <ag-slider
            label="Volume"
            value="75"
          ></ag-slider>
        </div>

        <div class="mbe4">
          <h2>Dual Range Slider</h2>
          <p class="mbs2 mbe3">Select a range with two thumbs</p>
        </div>
        <div class="mbe4">
          <ag-slider
            label="Price Range ($0 - $1000)"
            dual
            min="0"
            max="1000"
            .value="${[200, 800]}"
          ></ag-slider>
        </div>

        <div class="mbe4">
          <h2>Size Variants</h2>
          <p class="mbs2 mbe3">Three size options: small, default, and large</p>
        </div>
        <div
          class="mbe4"
          style="display: flex; flex-direction: column; gap: 2rem;"
        >
          <ag-slider
            label="Small"
            size="small"
            value="30"
          ></ag-slider>
          <ag-slider
            label="Default"
            size="default"
            value="50"
          ></ag-slider>
          <ag-slider
            label="Large"
            size="large"
            value="70"
          ></ag-slider>
        </div>

        <div class="mbe4">
          <h2>Style Variants</h2>
          <p class="mbs2 mbe3">Choose from filled and monochrome variants</p>
        </div>
        <div
          class="mbe4"
          style="display: flex; flex-direction: column; gap: 2rem;"
        >
          <fieldset style="border: 1px solid var(--ag-border); padding: 1rem; border-radius: var(--ag-radius-md);">
            <legend style="padding: 0 0.5rem; font-weight: 600;">Default (Primary Color)</legend>
            <ag-slider
              label="Standard"
              value="50"
            ></ag-slider>
          </fieldset>

          <fieldset style="border: 1px solid var(--ag-border); padding: 1rem; border-radius: var(--ag-radius-md);">
            <legend style="padding: 0 0.5rem; font-weight: 600;">Filled Variant</legend>
            <ag-slider
              label="Filled Thumb"
              filled
              value="60"
            ></ag-slider>
          </fieldset>

          <fieldset style="border: 1px solid var(--ag-border); padding: 1rem; border-radius: var(--ag-radius-md);">
            <legend style="padding: 0 0.5rem; font-weight: 600;">Monochrome (Adapts to Dark Mode)</legend>
            <ag-slider
              label="Monochrome"
              monochrome
              value="70"
            ></ag-slider>
          </fieldset>

          <fieldset style="border: 1px solid var(--ag-border); padding: 1rem; border-radius: var(--ag-radius-md);">
            <legend style="padding: 0 0.5rem; font-weight: 600;">Monochrome Filled</legend>
            <ag-slider
              label="Monochrome Filled"
              monochrome
              filled
              value="80"
            ></ag-slider>
          </fieldset>
        </div>

        <div class="mbe4">
          <h2>With Ticks and Tooltip</h2>
          <p class="mbs2 mbe3">Display tick marks and value tooltip</p>
        </div>
        <div
          class="mbe4"
          style="display: flex; flex-direction: column; gap: 2rem;"
        >
          <ag-slider
            label="With Tick Marks"
            show-ticks
            tick-step="25"
            value="50"
          ></ag-slider>
          <ag-slider
            label="With Tooltip"
            show-tooltip
            value="65"
          ></ag-slider>
          <ag-slider
            label="Ticks + Tooltip"
            show-ticks
            show-tooltip
            tick-step="20"
            value="40"
          ></ag-slider>
        </div>

        <div class="mbe4">
          <h2>Step Increments</h2>
          <p class="mbs2 mbe3">Control the granularity of value changes. Tick marks show the step intervals.</p>
        </div>
        <div
          class="mbe4"
          style="display: flex; flex-direction: column; gap: 2rem;"
        >
          <ag-slider
            label="Step 5"
            step="5"
            value="50"
            show-ticks
            tick-step="5"
          ></ag-slider>
          <ag-slider
            label="Step 10"
            step="10"
            value="50"
            show-ticks
            tick-step="10"
          ></ag-slider>
          <ag-slider
            label="Step 25 (Coarse)"
            step="25"
            value="50"
            show-ticks
            tick-step="25"
          ></ag-slider>
        </div>

        <div class="mbe4">
          <h2>Custom Range</h2>
          <p class="mbs2 mbe3">Configure min, max, and step values</p>
        </div>
        <div
          class="mbe4"
          style="display: flex; flex-direction: column; gap: 2rem;"
        >
          <ag-slider
            label="Temperature (°C)"
            min="-20"
            max="40"
            step="5"
            value="20"
          ></ag-slider>
          <ag-slider
            label="Percentage"
            min="0"
            max="100"
            step="1"
            value="75"
            help-text="Adjust from 0% to 100%"
          ></ag-slider>
        </div>

        <div class="mbe4">
          <h2>Vertical Orientation</h2>
          <p class="mbs2 mbe3">Vertical sliders for compact layouts</p>
        </div>
        <div
          class="mbe4"
          style="display: flex; gap: 2rem; align-items: flex-end;"
        >
          <ag-slider
            label="Volume"
            vertical
            value="75"
          ></ag-slider>
          <ag-slider
            label="Bass"
            vertical
            size="small"
            value="50"
          ></ag-slider>
          <ag-slider
            label="Treble"
            vertical
            size="large"
            value="60"
          ></ag-slider>
          <ag-slider
            label="Balance"
            vertical
            dual
            .value="${[25, 75]}"
          ></ag-slider>
        </div>

        <div class="mbe4">
          <h2>States</h2>
          <p class="mbs2 mbe3">Disabled, readonly, and invalid states</p>
        </div>
        <div
          class="mbe4"
          style="display: flex; flex-direction: column; gap: 2rem;"
        >
          <ag-slider
            label="Disabled"
            disabled
            value="50"
            help-text="This slider is disabled"
          ></ag-slider>
          <ag-slider
            label="Readonly"
            readonly
            value="65"
            help-text="This slider is readonly"
          ></ag-slider>
          <ag-slider
            label="Invalid"
            invalid
            error-message="Value must be at least 50"
            value="30"
          ></ag-slider>
        </div>

        <div class="mbe4">
          <h2>With Help Text</h2>
          <p class="mbs2 mbe3">Provide additional guidance with help text</p>
        </div>
        <div class="mbe4">
          <ag-slider
            label="Brightness"
            help-text="Adjust screen brightness level (affects battery life)"
            value="75"
          ></ag-slider>
        </div>

        <div class="mbe4">
          <h2>Event Testing</h2>
          <p class="mbs2 mbe3">
            Demonstrates the dual-dispatch event pattern: both CustomEvents (via @input/@change)
            and native events work simultaneously. Drag the slider to see real-time events.
          </p>
        </div>
        <div class="mbe4">
          <ag-slider
            label="Interactive Slider"
            .value="${this.eventValue}"
            @input="${this.handleInput}"
            @change="${this.handleChange}"
            @focus="${this.handleFocus}"
            @blur="${this.handleBlur}"
          ></ag-slider>
          <div
            class="event-log"
            style="margin-top: 1.5rem; padding: 1rem; background: var(--ag-background-secondary); border-radius: var(--ag-radius-md); border: 1px solid var(--ag-border);"
          >
            <h3 style="margin-top: 0; margin-bottom: 0.75rem; font-size: 0.95rem;">Event Log (last 10 events):</h3>
            ${this.events.length === 0 ? html`
              <div style="color: var(--ag-text-muted); font-size: 0.875rem; font-style: italic;">
                Interact with the slider to see events...
              </div>
            ` : this.events.map(event => html`
              <div style="font-family: monospace; font-size: 0.875rem; padding: 0.25rem 0; color: var(--ag-text-primary);">
                ${event}
              </div>
            `)}
          </div>
          <div style="margin-top: 1rem; font-size: 0.875rem; color: var(--ag-text-secondary);">
            Current value: <strong>${this.eventValue}</strong>
          </div>
        </div>

        <div class="mbe4">
          <h2>Dual Range Events</h2>
          <p class="mbs2 mbe3">Event handling with dual range sliders</p>
        </div>
        <div class="mbe4">
          <ag-slider
            label="Price Range Filter"
            dual
            min="0"
            max="1000"
            step="10"
            .value="${this.rangeValue}"
            @input="${this.handleRangeInput}"
            @change="${this.handleRangeChange}"
          ></ag-slider>
          <div
            class="event-log"
            style="margin-top: 1.5rem; padding: 1rem; background: var(--ag-background-secondary); border-radius: var(--ag-radius-md); border: 1px solid var(--ag-border);"
          >
            <h3 style="margin-top: 0; margin-bottom: 0.75rem; font-size: 0.95rem;">Range Event Log (last 10 events):</h3>
            ${this.rangeEvents.length === 0 ? html`
              <div style="color: var(--ag-text-muted); font-size: 0.875rem; font-style: italic;">
                Drag the range handles to see events...
              </div>
            ` : this.rangeEvents.map(event => html`
              <div style="font-family: monospace; font-size: 0.875rem; padding: 0.25rem 0; color: var(--ag-text-primary);">
                ${event}
              </div>
            `)}
          </div>
          <div style="margin-top: 1rem; font-size: 0.875rem; color: var(--ag-text-secondary);">
            Current range: <strong>$${this.rangeValue[0]} - $${this.rangeValue[1]}</strong>
          </div>
        </div>

        <div class="mbe4">
          <h2>CSS Shadow Parts Customization</h2>
          <p class="mbs2 mbe3">
            Use CSS Shadow Parts to customize the component's appearance.
            These examples show customized track, thumb, and label styling.
          </p>
        </div>
        <div
          class="mbe4"
          style="display: flex; flex-direction: column; gap: 2rem;"
        >
          <ag-slider
            label="Gradient Track"
            value="60"
            class="custom-slider-1"
          ></ag-slider>
          <ag-slider
            label="Large Thumb with Shadow"
            value="70"
            class="custom-slider-2"
          ></ag-slider>
          <ag-slider
            label="Fully Customized"
            value="50"
            class="custom-slider-3"
            help-text="Custom colors, sizing, and typography"
          ></ag-slider>
        </div>

        <div class="mbe4">
          <h2>External Label Support</h2>
          <p class="mbs2 mbe3">
            The Slider component supports external labels with helper text, required fields, and validation states.
          </p>
        </div>
        <div
          class="mbe4"
          style="max-width: 600px;"
        >
          <div class="mbe3">
            <ag-slider
              label="Volume Level"
              min="0"
              max="100"
              value="75"
            ></ag-slider>
          </div>

          <div class="mbe3">
            <ag-slider
              label="Brightness"
              help-text="Adjust screen brightness level (affects battery life)"
              min="0"
              max="100"
              value="50"
            ></ag-slider>
          </div>

          <div class="mbe3">
            <ag-slider
              label="Temperature"
              required
              help-text="This field is required"
              min="-20"
              max="40"
              value="20"
            ></ag-slider>
          </div>

          <div class="mbe3">
            <ag-slider
              label="Price Range"
              required
              invalid
              error-message="Please select a valid price range"
              min="0"
              max="1000"
              value="50"
            ></ag-slider>
          </div>
        </div>

        <div class="mbe4">
          <h2>Label Positioning</h2>
          <p class="mbs2 mbe3">
            Control label position with <code>label-position</code>: 'top' (default), 'start', 'end', or 'bottom'.
          </p>
        </div>
        <div
          class="mbe4"
          style="max-width: 600px;"
        >
          <div class="mbe3">
            <ag-slider
              label="Top Label (Default)"
              label-position="top"
              min="0"
              max="100"
              value="50"
            ></ag-slider>
          </div>

          <div class="mbe3">
            <ag-slider
              label="Start Position"
              label-position="start"
              min="0"
              max="100"
              value="60"
            ></ag-slider>
          </div>

          <div class="mbe3">
            <ag-slider
              label="End Position"
              label-position="end"
              min="0"
              max="100"
              value="70"
            ></ag-slider>
          </div>

          <div class="mbe3">
            <ag-slider
              label="Bottom Position"
              label-position="bottom"
              help-text="Bottom label position for alternative layouts"
              min="0"
              max="100"
              value="40"
            ></ag-slider>
          </div>
        </div>
      </section>
    `;
  }
}

// Register the custom element (at the bottom, NOT with decorator)
customElements.define('slider-lit-examples', SliderLitExamples);

Interactive Preview: Click the "Open in StackBlitz" button below to see this example running live in an interactive playground.

View React Code
import { useState } from "react";
import { ReactSlider } from "agnosticui-core/slider/react";

export default function SliderReactExamples() {
  const [eventValue, setEventValue] = useState(50);
  const [events, setEvents] = useState([]);
  const [rangeValue, setRangeValue] = useState([200, 800]);
  const [rangeEvents, setRangeEvents] = useState([]);

  const addEvent = (event) => {
    setEvents((prev) => [
      ...prev,
      `${new Date().toLocaleTimeString()}: ${event}`,
    ].slice(-10));
  };

  const addRangeEvent = (event) => {
    setRangeEvents((prev) => [
      ...prev,
      `${new Date().toLocaleTimeString()}: ${event}`,
    ].slice(-10));
  };

  const handleInput = (detail) => {
    setEventValue(detail.value);
    addEvent(`📝 input: value=${detail.value}`);
  };

  const handleChange = (detail) => {
    addEvent(`✅ change: value=${detail.value}`);
  };

  const handleFocus = () => {
    addEvent('🎯 focus');
  };

  const handleBlur = () => {
    addEvent('👋 blur');
  };

  const handleRangeInput = (detail) => {
    setRangeValue(detail.value);
    const [min, max] = detail.value;
    addRangeEvent(`📝 input: range=[$${min}, $${max}]`);
  };

  const handleRangeChange = (detail) => {
    const [min, max] = detail.value;
    addRangeEvent(`✅ change: range=[$${min}, $${max}]`);
  };

  return (
    <section>
      <div className="mbe4">
        <h2>Basic Slider</h2>
        <p className="mbs2 mbe3">Single value slider with label</p>
      </div>
      <div className="mbe4">
        <ReactSlider
          label="Volume"
          value={75}
        />
      </div>

      <div className="mbe4">
        <h2>Dual Range Slider</h2>
        <p className="mbs2 mbe3">Select a range with two thumbs</p>
      </div>
      <div className="mbe4">
        <ReactSlider
          label="Price Range ($0 - $1000)"
          dual={true}
          min={0}
          max={1000}
          value={[200, 800]}
        />
      </div>

      <div className="mbe4">
        <h2>Size Variants</h2>
        <p className="mbs2 mbe3">Three size options: small, default, and large</p>
      </div>
      <div
        className="mbe4"
        style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}
      >
        <ReactSlider
          label="Small"
          size="small"
          value={30}
        />
        <ReactSlider
          label="Default"
          size="default"
          value={50}
        />
        <ReactSlider
          label="Large"
          size="large"
          value={70}
        />
      </div>

      <div className="mbe4">
        <h2>Style Variants</h2>
        <p className="mbs2 mbe3">Choose from filled and monochrome variants</p>
      </div>
      <div
        className="mbe4"
        style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}
      >
        <fieldset style={{ border: '1px solid var(--ag-border)', padding: '1rem', borderRadius: 'var(--ag-radius-md)' }}>
          <legend style={{ padding: '0 0.5rem', fontWeight: 600 }}>Default (Primary Color)</legend>
          <ReactSlider
            label="Standard"
            value={50}
          />
        </fieldset>

        <fieldset style={{ border: '1px solid var(--ag-border)', padding: '1rem', borderRadius: 'var(--ag-radius-md)' }}>
          <legend style={{ padding: '0 0.5rem', fontWeight: 600 }}>Filled Variant</legend>
          <ReactSlider
            label="Filled Thumb"
            filled={true}
            value={60}
          />
        </fieldset>

        <fieldset style={{ border: '1px solid var(--ag-border)', padding: '1rem', borderRadius: 'var(--ag-radius-md)' }}>
          <legend style={{ padding: '0 0.5rem', fontWeight: 600 }}>Monochrome (Adapts to Dark Mode)</legend>
          <ReactSlider
            label="Monochrome"
            monochrome={true}
            value={70}
          />
        </fieldset>

        <fieldset style={{ border: '1px solid var(--ag-border)', padding: '1rem', borderRadius: 'var(--ag-radius-md)' }}>
          <legend style={{ padding: '0 0.5rem', fontWeight: 600 }}>Monochrome Filled</legend>
          <ReactSlider
            label="Monochrome Filled"
            monochrome={true}
            filled={true}
            value={80}
          />
        </fieldset>
      </div>

      <div className="mbe4">
        <h2>With Ticks and Tooltip</h2>
        <p className="mbs2 mbe3">Display tick marks and value tooltip</p>
      </div>
      <div
        className="mbe4"
        style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}
      >
        <ReactSlider
          label="With Tick Marks"
          showTicks={true}
          tickStep={25}
          value={50}
        />
        <ReactSlider
          label="With Tooltip"
          showTooltip={true}
          value={65}
        />
        <ReactSlider
          label="Ticks + Tooltip"
          showTicks={true}
          showTooltip={true}
          tickStep={20}
          value={40}
        />
      </div>

      <div className="mbe4">
        <h2>Step Increments</h2>
        <p className="mbs2 mbe3">Control the granularity of value changes. Tick marks show the step intervals.</p>
      </div>
      <div
        className="mbe4"
        style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}
      >
        <ReactSlider
          label="Step 5"
          step={5}
          value={50}
          showTicks={true}
          tickStep={5}
        />
        <ReactSlider
          label="Step 10"
          step={10}
          value={50}
          showTicks={true}
          tickStep={10}
        />
        <ReactSlider
          label="Step 25 (Coarse)"
          step={25}
          value={50}
          showTicks={true}
          tickStep={25}
        />
      </div>

      <div className="mbe4">
        <h2>Custom Range</h2>
        <p className="mbs2 mbe3">Configure min, max, and step values</p>
      </div>
      <div
        className="mbe4"
        style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}
      >
        <ReactSlider
          label="Temperature (°C)"
          min={-20}
          max={40}
          step={5}
          value={20}
        />
        <ReactSlider
          label="Percentage"
          min={0}
          max={100}
          step={1}
          value={75}
          helpText="Adjust from 0% to 100%"
        />
      </div>

      <div className="mbe4">
        <h2>Vertical Orientation</h2>
        <p className="mbs2 mbe3">Vertical sliders for compact layouts</p>
      </div>
      <div
        className="mbe4"
        style={{ display: 'flex', gap: '2rem', alignItems: 'flex-end' }}
      >
        <ReactSlider
          label="Volume"
          vertical={true}
          value={75}
        />
        <ReactSlider
          label="Bass"
          vertical={true}
          size="small"
          value={50}
        />
        <ReactSlider
          label="Treble"
          vertical={true}
          size="large"
          value={60}
        />
        <ReactSlider
          label="Balance"
          vertical={true}
          dual={true}
          value={[25, 75]}
        />
      </div>

      <div className="mbe4">
        <h2>States</h2>
        <p className="mbs2 mbe3">Disabled, readonly, and invalid states</p>
      </div>
      <div
        className="mbe4"
        style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}
      >
        <ReactSlider
          label="Disabled"
          disabled={true}
          value={50}
          helpText="This slider is disabled"
        />
        <ReactSlider
          label="Readonly"
          readonly={true}
          value={65}
          helpText="This slider is readonly"
        />
        <ReactSlider
          label="Invalid"
          invalid={true}
          errorMessage="Value must be at least 50"
          value={30}
        />
      </div>

      <div className="mbe4">
        <h2>With Help Text</h2>
        <p className="mbs2 mbe3">Provide additional guidance with help text</p>
      </div>
      <div className="mbe4">
        <ReactSlider
          label="Brightness"
          helpText="Adjust screen brightness level (affects battery life)"
          value={75}
        />
      </div>

      <div className="mbe4">
        <h2>Event Testing</h2>
        <p className="mbs2 mbe3">
          Demonstrates the dual-dispatch event pattern: both CustomEvents (via @input/@change)
          and native events work simultaneously. Drag the slider to see real-time events.
        </p>
      </div>
      <div className="mbe4">
        <ReactSlider
          label="Interactive Slider"
          value={eventValue}
          onInput={handleInput}
          onChange={handleChange}
          onFocus={handleFocus}
          onBlur={handleBlur}
        />
        <div
          className="event-log"
          style={{ marginTop: '1.5rem', padding: '1rem', background: 'var(--ag-background-secondary)', borderRadius: 'var(--ag-radius-md)', border: '1px solid var(--ag-border)' }}
        >
          <h3 style={{ marginTop: 0, marginBottom: '0.75rem', fontSize: '0.95rem' }}>Event Log (last 10 events):</h3>
          {events.length === 0 ? (
            <div style={{ color: 'var(--ag-text-muted)', fontSize: '0.875rem', fontStyle: 'italic' }}>
              Interact with the slider to see events...
            </div>
          ) : (
            events.map((event, i) => (
              <div
                key={i}
                style={{ fontFamily: 'monospace', fontSize: '0.875rem', padding: '0.25rem 0', color: 'var(--ag-text-primary)' }}
              >
                {event}
              </div>
            ))
          )}
        </div>
        <div style={{ marginTop: '1rem', fontSize: '0.875rem', color: 'var(--ag-text-secondary)' }}>
          Current value: <strong>{eventValue}</strong>
        </div>
      </div>

      <div className="mbe4">
        <h2>Dual Range Events</h2>
        <p className="mbs2 mbe3">Event handling with dual range sliders</p>
      </div>
      <div className="mbe4">
        <ReactSlider
          label="Price Range Filter"
          dual={true}
          min={0}
          max={1000}
          step={10}
          value={rangeValue}
          onInput={handleRangeInput}
          onChange={handleRangeChange}
        />
        <div
          className="event-log"
          style={{ marginTop: '1.5rem', padding: '1rem', background: 'var(--ag-background-secondary)', borderRadius: 'var(--ag-radius-md)', border: '1px solid var(--ag-border)' }}
        >
          <h3 style={{ marginTop: 0, marginBottom: '0.75rem', fontSize: '0.95rem' }}>Range Event Log (last 10 events):</h3>
          {rangeEvents.length === 0 ? (
            <div style={{ color: 'var(--ag-text-muted)', fontSize: '0.875rem', fontStyle: 'italic' }}>
              Drag the range handles to see events...
            </div>
          ) : (
            rangeEvents.map((event, i) => (
              <div
                key={i}
                style={{ fontFamily: 'monospace', fontSize: '0.875rem', padding: '0.25rem 0', color: 'var(--ag-text-primary)' }}
              >
                {event}
              </div>
            ))
          )}
        </div>
        <div style={{ marginTop: '1rem', fontSize: '0.875rem', color: 'var(--ag-text-secondary)' }}>
          Current range: <strong>${rangeValue[0]} - ${rangeValue[1]}</strong>
        </div>
      </div>

      <div className="mbe4">
        <h2>CSS Shadow Parts Customization</h2>
        <p className="mbs2 mbe3">
          Use CSS Shadow Parts to customize the component's appearance.
          These examples show customized track, thumb, and label styling.
        </p>
      </div>
      <div
        className="mbe4"
        style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}
      >
        <ReactSlider
          label="Gradient Track"
          value={60}
          className="custom-slider-1"
        />
        <ReactSlider
          label="Large Thumb with Shadow"
          value={70}
          className="custom-slider-2"
        />
        <ReactSlider
          label="Fully Customized"
          value={50}
          className="custom-slider-3"
          helpText="Custom colors, sizing, and typography"
        />
      </div>

      <div className="mbe4">
        <h2>External Label Support</h2>
        <p className="mbs2 mbe3">
          The Slider component supports external labels with helper text, required fields, and validation states.
        </p>
      </div>
      <div
        className="mbe4"
        style={{ maxWidth: '600px' }}
      >
        <div className="mbe3">
          <ReactSlider
            label="Volume Level"
            min={0}
            max={100}
            value={75}
          />
        </div>

        <div className="mbe3">
          <ReactSlider
            label="Brightness"
            helpText="Adjust screen brightness level (affects battery life)"
            min={0}
            max={100}
            value={50}
          />
        </div>

        <div className="mbe3">
          <ReactSlider
            label="Temperature"
            required={true}
            helpText="This field is required"
            min={-20}
            max={40}
            value={20}
          />
        </div>

        <div className="mbe3">
          <ReactSlider
            label="Price Range"
            required={true}
            invalid={true}
            errorMessage="Please select a valid price range"
            min={0}
            max={1000}
            value={50}
          />
        </div>
      </div>

      <div className="mbe4">
        <h2>Label Positioning</h2>
        <p className="mbs2 mbe3">
          Control label position with <code>label-position</code>: 'top' (default), 'start', 'end', or 'bottom'.
        </p>
      </div>
      <div
        className="mbe4"
        style={{ maxWidth: '600px' }}
      >
        <div className="mbe3">
          <ReactSlider
            label="Top Label (Default)"
            labelPosition="top"
            min={0}
            max={100}
            value={50}
          />
        </div>

        <div className="mbe3">
          <ReactSlider
            label="Start Position"
            labelPosition="start"
            min={0}
            max={100}
            value={60}
          />
        </div>

        <div className="mbe3">
          <ReactSlider
            label="End Position"
            labelPosition="end"
            min={0}
            max={100}
            value={70}
          />
        </div>

        <div className="mbe3">
          <ReactSlider
            label="Bottom Position"
            labelPosition="bottom"
            helpText="Bottom label position for alternative layouts"
            min={0}
            max={100}
            value={40}
          />
        </div>
      </div>
    </section>
  );
}
Open in StackBlitz

Usage

TIP

The framework examples below import AgnosticUI as an npm package. Alternatively, you can use the CLI for complete control, AI/LLM visibility, and full code ownership:

bash
npx ag init --framework FRAMEWORK # react, vue, lit, svelte, etc.
npx ag add Slider

The CLI copies source code directly into your project, giving you full visibility and control. After running npx ag add, you'll receive exact import instructions.

Vue
vue
<template>
  <section>
    <!-- Basic slider -->
    <VueSlider label="Volume" :min="0" :max="100" :value="75" />

    <!-- Dual range slider -->
    <VueSlider
      label="Price Range"
      dual
      :min="0"
      :max="1000"
      :value="[200, 800]"
    />

    <!-- With ticks and tooltip -->
    <VueSlider
      label="Brightness"
      :min="0"
      :max="100"
      :step="25"
      :value="50"
      show-ticks
      show-tooltip
      :tick-step="25"
    />

    <!-- Filled and monochrome variants -->
    <VueSlider label="Filled Variant" filled :value="60" />
    <VueSlider label="Monochrome" monochrome :value="70" />
  </section>
</template>

<script lang="ts">
import { defineComponent } from "vue";
import { VueSlider } from "agnosticui-core/slider/vue";

export default defineComponent({
  components: { VueSlider },
});
</script>
React
tsx
import { ReactSlider } from "agnosticui-core/slider/react";

export default function Example() {
  const [value, setValue] = useState(50);
  const [range, setRange] = useState<number | [number, number] | undefined>([
    200, 800,
  ]);

  return (
    <section>
      {/* Basic slider */}
      <ReactSlider
        label="Volume"
        min={0}
        max={100}
        value={value}
        onInput={(e) => setValue(e.detail.value as number)}
      />

      {/* Dual range slider */}
      <ReactSlider
        label="Price Range"
        dual
        min={0}
        max={1000}
        value={range}
        onInput={(e) => setRange(e.detail.value as [number, number])}
      />

      {/* With ticks and tooltip */}
      <ReactSlider
        label="Brightness"
        min={0}
        max={100}
        step={25}
        value={50}
        showTicks
        showTooltip
        tickStep={25}
      />

      {/* Filled and monochrome variants */}
      <ReactSlider label="Filled Variant" filled value={60} />
      <ReactSlider label="Monochrome" monochrome value={70} />
    </section>
  );
}
Lit (Web Components)
html
<script type="module">
  import "agnosticui-core/slider";
</script>

<section>
  <!-- Basic slider -->
  <ag-slider label="Volume" min="0" max="100" value="75"></ag-slider>

  <!-- Dual range slider -->
  <ag-slider
    label="Price Range"
    dual
    min="0"
    max="1000"
    .value="${[200,"
    800]}
  ></ag-slider>

  <!-- With ticks and tooltip -->
  <ag-slider
    label="Brightness"
    min="0"
    max="100"
    step="25"
    value="50"
    show-ticks
    show-tooltip
    tick-step="25"
  ></ag-slider>

  <!-- Filled and monochrome variants -->
  <ag-slider label="Filled Variant" filled value="60"></ag-slider>

  <ag-slider label="Monochrome" monochrome value="70"></ag-slider>
</section>

Props

PropTypeDefaultDescription
labelstring''Label text for the slider
labelPosition'top' | 'start' | 'end' | 'bottom''top'Position of the label relative to the slider
labelHiddenbooleanfalseVisually hide the label (still accessible)
noLabelbooleanfalseRemove label completely
ariaLabelstring''ARIA label when label is hidden
minnumber0Minimum value
maxnumber100Maximum value
stepnumber1Step increment for value changes
valuenumber | [number, number]0Current value (single or dual range)
dualbooleanfalseEnable dual range mode with two thumbs
verticalbooleanfalseVertical orientation
filledbooleanfalseFilled thumb style (solid background)
monochromebooleanfalseMonochrome color scheme (adapts to dark mode)
size'small' | 'default' | 'large''default'Size variant
disabledbooleanfalseDisabled state prevents interaction
readonlybooleanfalseReadonly state allows focus but prevents changes
requiredbooleanfalseRequired field indicator
invalidbooleanfalseInvalid state for validation feedback
errorMessagestring''Error message text displayed when invalid
helpTextstring''Helper text displayed below slider
namestring''Form field name for submission
showTooltipbooleanfalseShow current value in tooltip while dragging
showTicksbooleanfalseDisplay tick marks along track
tickStepnumber25Interval for tick marks

Events

EventFrameworkDetailDescription
inputVue: @input
React: onInput
Lit: @input
{ value: number | [number, number] }Fired continuously while dragging the thumb. Use for real-time updates.
changeVue: @change
React: onChange
Lit: @change
{ value: number | [number, number] }Fired when the thumb is released after dragging. Use for final value updates.
focusVue: @focus
React: onFocus
Lit: @focus
FocusEventFired when slider receives focus.
blurVue: @blur
React: onBlur
Lit: @blur
FocusEventFired when slider loses focus.

Note: The Slider component supports dual-dispatch event propagation: it dispatches both DOM CustomEvents (usable with addEventListener) and invokes callback props (.onInput, .onChange), giving you flexibility in how you handle events.

Event Usage Examples

Vue
vue
<template>
  <section>
    <!-- Event handler with @input -->
    <VueSlider
      label="Volume"
      :value="volume"
      @input="handleInput"
      @change="handleChange"
    />

    <!-- v-model:value for two-way binding -->
    <VueSlider label="Brightness" v-model:value="brightness" />

    <!-- Dual range with events -->
    <VueSlider
      label="Price Range"
      dual
      :value="priceRange"
      @input="handleRangeInput"
      @change="handleRangeChange"
    />

    <p>Current volume: {{ volume }}</p>
    <p>Price range: ${{ priceRange[0] }} - ${{ priceRange[1] }}</p>
  </section>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { VueSlider } from "agnosticui-core/slider/vue";

const volume = ref(50);
const brightness = ref(75);
const priceRange = ref([200, 800]);

const handleInput = (detail: { value: number | [number, number] }) => {
  volume.value = detail.value as number;
  console.log("Input (dragging):", detail.value);
};

const handleChange = (detail: { value: number | [number, number] }) => {
  console.log("Change (released):", detail.value);
};

const handleRangeInput = (detail: { value: number | [number, number] }) => {
  priceRange.value = detail.value as [number, number];
};

const handleRangeChange = (detail: { value: number | [number, number] }) => {
  console.log("Range finalized:", detail.value);
};
</script>
React
tsx
import { useState } from "react";
import { ReactSlider } from "agnosticui-core/slider/react";

export default function Example() {
  const [volume, setVolume] = useState(50);
  const [priceRange, setPriceRange] = useState([200, 800]);

  return (
    <section>
      {/* Event handler with onInput */}
      <ReactSlider
        label="Volume"
        value={volume}
        onInput={(e) => {
          setVolume(e.detail.value as number);
          console.log("Input (dragging):", e.detail.value);
        }}
        onChange={(e) => {
          console.log("Change (released):", e.detail.value);
        }}
      />

      {/* Dual range with events */}
      <ReactSlider
        label="Price Range"
        dual
        value={priceRange}
        onInput={(e) => {
          setPriceRange(e.detail.value as [number, number]);
        }}
        onChange={(e) => {
          console.log("Range finalized:", e.detail.value);
        }}
      />

      {/* With focus/blur handlers */}
      <ReactSlider
        label="Brightness"
        value={75}
        onFocus={() => console.log("Slider focused")}
        onBlur={() => console.log("Slider blurred")}
        onInput={(e) => console.log("Value:", e.detail.value)}
      />

      <p>Current volume: {volume}</p>
      <p>
        Price range: ${priceRange[0]} - ${priceRange[1]}
      </p>
    </section>
  );
}
Lit (Web Components)
html
<script type="module">
  import "agnosticui-core/slider";

  // Pattern 1: addEventListener (DOM events)
  const slider1 = document.querySelector("#slider1");
  slider1.addEventListener("input", (e) => {
    console.log("Input event (dragging):", e.detail.value);
  });
  slider1.addEventListener("change", (e) => {
    console.log("Change event (released):", e.detail.value);
  });

  // Pattern 2: Callback props
  const slider2 = document.querySelector("#slider2");
  slider2.onInput = (e) => {
    console.log("Input callback:", e.detail.value);
  };
  slider2.onChange = (e) => {
    console.log("Change callback:", e.detail.value);
  };

  // Pattern 3: Both patterns work (dual-dispatch)
  const slider3 = document.querySelector("#slider3");
  slider3.addEventListener("input", (e) => {
    console.log("DOM event:", e.detail.value);
  });
  slider3.onInput = (e) => {
    console.log("Callback also fired:", e.detail.value);
  };

  // Focus and blur events
  const slider4 = document.querySelector("#slider4");
  slider4.addEventListener("focus", (e) => {
    console.log("Slider focused");
  });
  slider4.addEventListener("blur", (e) => {
    console.log("Slider blurred");
  });
</script>

<section>
  <ag-slider
    id="slider1"
    label="addEventListener pattern"
    value="50"
  ></ag-slider>

  <ag-slider id="slider2" label="Callback prop pattern" value="50"></ag-slider>

  <ag-slider
    id="slider3"
    label="Dual-dispatch (both patterns)"
    value="50"
  ></ag-slider>

  <ag-slider
    id="slider4"
    label="With focus/blur handlers"
    value="50"
  ></ag-slider>
</section>

Type:

ts
export type SliderInputEvent = CustomEvent<{
  value: number | [number, number];
}>;
export type SliderChangeEvent = CustomEvent<{
  value: number | [number, number];
}>;

CSS Shadow Parts

Shadow Parts allow you to style internal elements of the slider from outside the shadow DOM using the ::part() CSS selector.

PartDescription
ag-slider-containerThe outer container wrapper
ag-slider-labelThe label element
ag-slider-trackThe slider track background
ag-slider-progressThe progress/fill indicator
ag-slider-thumbThe draggable thumb element
ag-slider-ticksContainer for tick marks (when showTicks is true)
ag-slider-tickIndividual tick mark
ag-slider-tooltipTooltip showing current value (when showTooltip is true)
ag-slider-help-textHelp text element
ag-slider-errorError message element

Customization Examples

css
/* Customize track height and color */
ag-slider::part(ag-slider-track) {
  height: 8px;
  background: var(--ag-background-tertiary);
  border-radius: var(--ag-radius-full);
}

/* Customize progress/fill color */
ag-slider::part(ag-slider-progress) {
  background: linear-gradient(90deg, var(--ag-green-500), var(--ag-blue-500));
}

/* Customize thumb appearance */
ag-slider::part(ag-slider-thumb) {
  width: 24px;
  height: 24px;
  border: 4px solid var(--ag-primary);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}

/* Style the label */
ag-slider::part(ag-slider-label) {
  font-weight: 700;
  color: var(--ag-primary);
  font-size: 1.1rem;
}

/* Customize tooltip */
ag-slider::part(ag-slider-tooltip) {
  background: var(--ag-primary);
  color: var(--ag-white);
  padding: 0.5rem 0.75rem;
  border-radius: var(--ag-radius-md);
  font-weight: 600;
}

Accessibility

  • Slider uses native <input type="range"> for proper keyboard navigation
  • Full keyboard support:
    • Arrow keys: Increment/decrement by step
    • Page Up/Down: Larger increments
    • Home/End: Jump to min/max
    • Tab: Navigate between thumbs in dual mode
  • Proper aria-valuenow, aria-valuemin, aria-valuemax attributes
  • Live region announcements when value changes (for screen readers)
  • Focus visible with customizable focus ring using design tokens
  • Disabled state prevents interaction and is communicated to assistive technologies

Dark Mode Support

All variants automatically support dark mode through CSS design tokens:

  • Default: Uses --ag-primary which adapts to dark mode
  • Monochrome: Uses --ag-text-primary for progress and borders
    • Light mode: Dark gray/black appearance
    • Dark mode: Light gray/white appearance
  • Monochrome + Filled: Thumb background adapts using --ag-text-primary
    • Light mode: Dark filled thumb
    • Dark mode: Light filled thumb for visibility
  • Thumb always uses --ag-white background to ensure visibility in both modes

Notes

  • Dual Range: When dual={true}, value must be a tuple [min, max]
  • Form Integration: Sliders work with standard HTML forms via ElementInternals API
  • Step Snapping: Values automatically snap to nearest step increment
  • Vertical Mode: Set vertical={true} for vertical orientation (useful for volume controls)
  • Ticks: Configure showTicks={true} and tickStep for visual markers
  • Tooltip: Set showTooltip={true} to show current value while dragging
  • Lit: Properties can be set via attributes or property binding (e.g., .value=${50})
  • All three implementations (Lit, React, Vue) share the same underlying styles and behavior