Slider

Component

Interactive examples and API documentation

Basic
Single value slider with tooltip.
Code
<%= render Hakumi::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 Hakumi::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 Hakumi::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 Hakumi::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 Hakumi::Space::Component.new(size: :large, direction: :vertical, align: :start) do %>
  <%= render Hakumi::Slider::Component.new(
    name: "disabled_slider",
    value: 60,
    min: 0,
    max: 100,
    step: 5,
    disabled: true,
    tooltip_visible: true
  ) %>

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

  <%= render Hakumi::Space::Component.new(size: :middle, direction: :vertical, align: :start) do %>
    <p style="margin-bottom: 8px;">Control value programmatically:</p>
    <%= render Hakumi::Space::Component.new(size: :middle, align: :center) do %>
      <div style="width: 200px; flex-shrink: 0;">
        <%= render Hakumi::Slider::Component.new(
          wrapper_id: "slider-value-demo",
          value: 50
        ) %>
      </div>
      <%= render Hakumi::Button::Component.new(
        size: :small,
        onclick: "document.getElementById('slider-value-demo').hakumiComponent.api.setValue(0)"
      ) do %>
        Set to 0
      <% end %>
      <%= render Hakumi::Button::Component.new(
        size: :small,
        onclick: "document.getElementById('slider-value-demo').hakumiComponent.api.setValue(100)"
      ) do %>
        Set to 100
      <% end %>
      <%= render Hakumi::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 Hakumi::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 Hakumi::Space::Component.new(size: :middle, align: :center) do %>
    <div style="flex: 1; min-width: 200px;">
      <%= render Hakumi::Slider::Component.new(
        wrapper_id: "slider-sync-demo",
        value: 50,
        min: 0,
        max: 100,
        step: 1
      ) %>
    </div>
    <%= render Hakumi::InputNumber::Component.new(
      name: "input_sync_demo",
      wrapper_id: "input-sync-demo",
      value: 50,
      min: 0,
      max: 100,
      step: 1,
      style: "width: 80px;"
    ) %>
  <% end %>

  <%= render Hakumi::Space::Component.new(size: :middle, 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 Hakumi::Space::Component.new(size: :middle, align: :center) do %>
      <%= render Hakumi::InputNumber::Component.new(
        name: "range_min_demo",
        wrapper_id: "range-min-demo",
        value: 20,
        min: 0,
        max: 80,
        step: 1,
        style: "width: 80px;"
      ) %>
      <div style="flex: 1; min-width: 200px;">
        <%= render Hakumi::Slider::Component.new(
          wrapper_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 Hakumi::InputNumber::Component.new(
        name: "range_max_demo",
        wrapper_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 - `class`, `id`, `data`, `wrapper_id`, `wrapper_class`, etc.

Slider JavaScript API (element.hakumiComponent.api)

Prop Type Default Description
getValue() Function - Return current value (number or [min, max] for range).
setValue(value) Function - Set value (number or [min, max] for range).
reset() Function - Reset to default value.
focus() Function - Focus first handle.
blur() Function - Blur all handles.
setDisabled(disabled) Function - Set disabled state (true/false).
isDisabled() Function - Return current disabled state.
toggleDisabled() Function - Toggle disabled state.
disable() Function - Set disabled to true.
enable() Function - Set disabled to false.
getMin() Function - Return minimum value.
getMax() Function - Return maximum value.
getStep() Function - Return step size.
isRange() Function - Return true if range mode.
isVertical() Function - Return true if vertical layout.