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: :middle, direction: :vertical, align: :start) do %>
    <p style="margin-bottom: 8px;">Toggle disabled state via API:</p>
    <%= render HakumiComponents::Space::Component.new(size: :middle, 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: :middle, direction: :vertical, align: :start) do %>
    <p style="margin-bottom: 8px;">Control value programmatically:</p>
    <%= render HakumiComponents::Space::Component.new(size: :middle, 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: :middle, 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: :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 HakumiComponents::Space::Component.new(size: :middle, 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.

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.