Slider

Component

Interactive examples and API documentation

Basic
Single value slider with tooltip.
Code
<%= render HakumiComponents::Slider::Component.new(
  name: "volume",
  value: 30,
  min: 0,
  max: 100,
  step: 5,
  tooltip_visible: true
) %>
Range
Two handles for selecting a range.
Code
<%= render HakumiComponents::Slider::Component.new(
  name: "range",
  range: true,
  value: [20, 80],
  min: 0,
  max: 100,
  step: 5,
  tooltip_visible: true
) %>
Vertical
Vertical slider layout.
Code
<div style="height: 240px;">
  <%= render HakumiComponents::Slider::Component.new(
    name: "vertical_volume",
    vertical: true,
    value: 40,
    min: 0,
    max: 100,
    step: 10,
    tooltip_visible: true
  ) %>
</div>
Marks & Dots
Show marks and step dots.
0 20 40 60 80 100
Code
<%= render HakumiComponents::Slider::Component.new(
  name: "marks",
  value: 40,
  min: 0,
  max: 100,
  step: 10,
  marks: {
    0 => "0",
    20 => "20",
    40 => "40",
    60 => "60",
    80 => "80",
    100 => "100"
  },
  dots: true,
  tooltip_visible: true
) %>
Disabled
Disabled slider state.

Toggle disabled state via API:

Control value programmatically:

Code
<%= render HakumiComponents::Space::Component.new(size: :large, direction: :vertical, align: :start) do %>
  <%= render HakumiComponents::Slider::Component.new(
    name: "disabled_slider",
    value: 60,
    min: 0,
    max: 100,
    step: 5,
    disabled: true,
    tooltip_visible: true
  ) %>

  <%= render HakumiComponents::Space::Component.new(size: :default, direction: :vertical, align: :start) do %>
    <p style="margin-bottom: 8px;">Toggle disabled state via API:</p>
    <%= render HakumiComponents::Space::Component.new(size: :default, align: :center) do %>
      <div style="width: 200px; flex-shrink: 0;">
        <%= render HakumiComponents::Slider::Component.new(
          id: "slider-disabled-demo",
          value: 30,
          disabled: true
        ) %>
      </div>
      <%= render HakumiComponents::Button::Component.new(
        size: :small,
        onclick: "document.getElementById('slider-disabled-demo').hakumiComponent?.api.toggleDisabled()"
      ) do %>
        Toggle Disabled
      <% end %>
    <% end %>
  <% end %>

  <%= render HakumiComponents::Space::Component.new(size: :default, direction: :vertical, align: :start) do %>
    <p style="margin-bottom: 8px;">Control value programmatically:</p>
    <%= render HakumiComponents::Space::Component.new(size: :default, align: :center) do %>
      <div style="width: 200px; flex-shrink: 0;">
        <%= render HakumiComponents::Slider::Component.new(
          id: "slider-value-demo",
          value: 50
        ) %>
      </div>
      <%= render HakumiComponents::Button::Component.new(
        size: :small,
        onclick: "document.getElementById('slider-value-demo').hakumiComponent?.api.setValue(0)"
      ) do %>
        Set to 0
      <% end %>
      <%= render HakumiComponents::Button::Component.new(
        size: :small,
        onclick: "document.getElementById('slider-value-demo').hakumiComponent?.api.setValue(100)"
      ) do %>
        Set to 100
      <% end %>
      <%= render HakumiComponents::Button::Component.new(
        size: :small,
        onclick: "document.getElementById('slider-value-demo').hakumiComponent?.api.reset()"
      ) do %>
        Reset
      <% end %>
    <% end %>
  <% end %>
<% end %>
Synchronized with InputNumber
Slider and InputNumber components synchronized via public API.

Both components stay in sync through their public APIs. Try dragging the slider or typing in the input.

Range slider synchronized with two inputs (inputs limit each other):

Code
<%= render HakumiComponents::Space::Component.new(size: :large, direction: :vertical) do %>
  <p style="margin-bottom: 8px; color: var(--color-text-secondary);">
    Both components stay in sync through their public APIs. Try dragging the slider or typing in the input.
  </p>

  <%= render HakumiComponents::Space::Component.new(size: :default, align: :center) do %>
    <div style="flex: 1; min-width: 200px;">
      <%= render HakumiComponents::Slider::Component.new(
        id: "slider-sync-demo",
        value: 50,
        min: 0,
        max: 100,
        step: 1
      ) %>
    </div>
    <%= render HakumiComponents::InputNumber::Component.new(
      name: "input_sync_demo",
      id: "input-sync-demo",
      value: 50,
      min: 0,
      max: 100,
      step: 1,
      style: "width: 80px;"
    ) %>
  <% end %>

  <%= render HakumiComponents::Space::Component.new(size: :default, direction: :vertical, align: :start) do %>
    <p style="margin: 16px 0 8px; color: var(--color-text-secondary);">
      Range slider synchronized with two inputs (inputs limit each other):
    </p>
    <%= render HakumiComponents::Space::Component.new(size: :default, align: :center) do %>
      <%= render HakumiComponents::InputNumber::Component.new(
        name: "range_min_demo",
        id: "range-min-demo",
        value: 20,
        min: 0,
        max: 80,
        step: 1,
        style: "width: 80px;"
      ) %>
      <div style="flex: 1; min-width: 200px;">
        <%= render HakumiComponents::Slider::Component.new(
          id: "slider-range-sync-demo",
          value: [20, 80],
          range: true,
          allow_cross: false,
          min: 0,
          max: 100,
          step: 1,
          handle_min: [0, 20],
          handle_max: [80, 100]
        ) %>
      </div>
      <%= render HakumiComponents::InputNumber::Component.new(
        name: "range_max_demo",
        id: "range-max-demo",
        value: 80,
        min: 20,
        max: 100,
        step: 1,
        style: "width: 80px;"
      ) %>
    <% end %>
  <% end %>
<% end %>

<script>
(function() {
  requestAnimationFrame(function() {
    setTimeout(function() {
      // Flag to prevent infinite loops
      let syncing = false;

      // Single slider sync
      const slider = document.getElementById('slider-sync-demo');
      const input = document.getElementById('input-sync-demo');

      if (slider && input) {
        slider.addEventListener('hakumi--slider:change', function(e) {
          if (syncing) return;
          syncing = true;
          if (input.hakumiComponent?.api) {
            input.hakumiComponent?.api.setValue(e.detail.value);
          }
          syncing = false;
        });

        input.addEventListener('hakumi--input-number:change', function(e) {
          if (syncing) return;
          syncing = true;
          if (slider.hakumiComponent?.api) {
            slider.hakumiComponent?.api.setValue(e.detail.value);
          }
          syncing = false;
        });
      }

      // Range slider sync with dynamic limits
      let syncingRange = false;
      const rangeSlider = document.getElementById('slider-range-sync-demo');
      const minInput = document.getElementById('range-min-demo');
      const maxInput = document.getElementById('range-max-demo');
      const minInputEl = minInput?.querySelector('input');
      const maxInputEl = maxInput?.querySelector('input');

      if (rangeSlider && minInput && maxInput) {
        // Update input limits based on current values
        const updateInputLimits = function(minVal, maxVal) {
          // Min input can go up to maxVal
          if (minInputEl) minInputEl.max = maxVal;
          // Max input can go down to minVal
          if (maxInputEl) maxInputEl.min = minVal;
        };

        // Slider -> InputNumbers
        rangeSlider.addEventListener('hakumi--slider:change', function(e) {
          if (syncingRange) return;
          syncingRange = true;
          const [min, max] = e.detail.value;
          if (minInput.hakumiComponent?.api) minInput.hakumiComponent?.api.setValue(min);
          if (maxInput.hakumiComponent?.api) maxInput.hakumiComponent?.api.setValue(max);
          updateInputLimits(min, max);
          syncingRange = false;
        });

        // Min Input -> Slider
        minInput.addEventListener('hakumi--input-number:change', function(e) {
          if (syncingRange) return;
          syncingRange = true;
          const minVal = parseFloat(e.detail.value) || 0;
          const maxVal = maxInput.hakumiComponent?.api?.getValue() || 100;
          // Clamp min to not exceed max
          const clampedMin = Math.min(minVal, maxVal);
          if (rangeSlider.hakumiComponent?.api) {
            rangeSlider.hakumiComponent?.api.setValue([clampedMin, maxVal]);
          }
          if (minVal !== clampedMin && minInput.hakumiComponent?.api) {
            minInput.hakumiComponent?.api.setValue(clampedMin);
          }
          updateInputLimits(clampedMin, maxVal);
          syncingRange = false;
        });

        // Max Input -> Slider
        maxInput.addEventListener('hakumi--input-number:change', function(e) {
          if (syncingRange) return;
          syncingRange = true;
          const maxVal = parseFloat(e.detail.value) || 100;
          const minVal = minInput.hakumiComponent?.api?.getValue() || 0;
          // Clamp max to not go below min
          const clampedMax = Math.max(maxVal, minVal);
          if (rangeSlider.hakumiComponent?.api) {
            rangeSlider.hakumiComponent?.api.setValue([minVal, clampedMax]);
          }
          if (maxVal !== clampedMax && maxInput.hakumiComponent?.api) {
            maxInput.hakumiComponent?.api.setValue(clampedMax);
          }
          updateInputLimits(minVal, clampedMax);
          syncingRange = false;
        });
      }
    }, 50);
  });
})();
</script>

Slider Props

Prop Type Default Description
name String or Symbol auto Input name/id.
label String - Optional label above the control.
caption String - Helper text displayed below.
value Numeric or Array - Current value or [min, max].
default_value Numeric or Array - Initial value if value is nil.
min Numeric 0 Minimum value.
max Numeric 100 Maximum value.
step Numeric 1 Step size.
range Boolean false Enable range handles.
allow_cross Boolean true Allow range handles to cross each other.
handle_min Array - Per-handle minimum values [leftMin, rightMin].
handle_max Array - Per-handle maximum values [leftMax, rightMax].
vertical Boolean false Vertical layout.
marks Hash or Array - Mark labels per value.
dots Boolean false Show dots at each step.
tooltip_visible Boolean false Always show tooltip.
keyboard Boolean true Enable keyboard control.
disabled Boolean false Disable interactions.
standalone Boolean true Render without FormItem wrapper.
required Boolean false Add required attribute.
errors Array[String] or String [] Error messages.
**html_attrs Keyword args - `id`, `class`, `data`, etc. Standard HTML attributes passed to wrapper.

JavaScript API

Access via element.hakumiComponent.api or HakumiComponents.get(element.id).
Prop Type Default Description
getValue() Method - Return the current value, or [min, max] for range sliders.
setValue(value) Method - Set the value, clamped and snapped to the configured range and step.
reset() Method - Reset to default_value, or to the component minimum/maximum fallback.
focus() Method - Focus the first handle.
blur() Method - Blur all handles.
setDisabled(disabled) Method - Set whether the slider is disabled.
isDisabled() Method - Return true when the slider is disabled.
toggleDisabled() Method - Toggle the disabled state.
disable() Method - Disable the slider.
enable() Method - Enable the slider.
getMin() Method - Return the configured minimum value.
getMax() Method - Return the configured maximum value.
getStep() Method - Return the configured step size.
isRange() Method - Return true when range mode is enabled.
isVertical() Method - Return true when vertical layout is enabled.