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.
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. |