Separator
stimeo--separator
Gives a divider real meaning, and can make it a focusable one that carries a value.
Turns a plain line into a divider with meaning, including whether it runs across or down, so a screen reader can convey the structure. Give it a value and it becomes a focusable divider: the arrow keys adjust it, Home and End jump to the ends, and the value is announced as it changes. Actually resizing the areas either side is not done here, so reach for the resizable part when you need that.
The thin line below is a decorative separator (role="separator" semantics only — nothing to operate).
Below is a vertical, focusable, value-bearing separator: Tab to it and use ← / → to change the left/right split (for mouse-drag resizing, use the Resizable component instead).
Here is a horizontal value-bearing separator: Tab to it and use ↑ / ↓ to change the top/bottom split.
Keyboard
| Key | Action |
|---|---|
| → / ↑ | Increase the value (ArrowRight when vertical, ArrowUp when horizontal). Focusable variant. |
| ← / ↓ | Decrease the value (ArrowLeft when vertical, ArrowDown when horizontal). Focusable variant. |
| Home / End | Jump to the minimum / maximum. Focusable variant. |
<%# Separator demo markup. The first separator is decorative; the other two are
focusable and value-bearing, dispatching change as the arrow keys adjust them.
The consumer-owned demo.js and demo.css turn that value into pane sizes and
the divider's appearance. %>
<div class="separator-demo">
<p class="separator-demo__text"><%= t("components.separator.demo.intro") %></p>
<div
class="separator separator--horizontal"
data-controller="stimeo--separator"
data-stimeo--separator-orientation-value="horizontal"></div>
<p class="separator-demo__text"><%= t("components.separator.demo.after") %></p>
<div class="separator-demo__split">
<%# The primary pane. Its width follows --separator-split, so the separator's
Value reports this pane's size: ArrowRight raises the value while moving
the vertical divider right. %>
<div class="separator-demo__pane" id="separator-demo-primary">
<%= t("components.separator.demo.left") %>
</div>
<div
class="separator separator--vertical"
<%# Reuse the primary pane's visible text as the separator's name. %>
aria-labelledby="separator-demo-primary"
aria-controls="separator-demo-primary"
data-controller="stimeo--separator"
data-stimeo--separator-orientation-value="vertical"
data-stimeo--separator-focusable-value="true"
data-stimeo--separator-min-value="20"
data-stimeo--separator-max-value="80"
data-stimeo--separator-step-value="5"
data-stimeo--separator-value-value="50"
data-action="keydown->stimeo--separator#onKeydown"></div>
<div class="separator-demo__pane"><%= t("components.separator.demo.right") %></div>
</div>
<p class="separator-demo__text"><%= t("components.separator.demo.after_horizontal") %></p>
<%# A horizontal separator takes ArrowUp and ArrowDown. demo.js maps the same
--separator-split custom property onto the top / bottom pane ratio. %>
<div class="separator-demo__split separator-demo__split--stacked">
<div class="separator-demo__pane"><%= t("components.separator.demo.top") %></div>
<div
class="separator separator--horizontal-handle"
aria-labelledby="separator-demo-primary-stacked"
aria-controls="separator-demo-primary-stacked"
data-controller="stimeo--separator"
data-stimeo--separator-orientation-value="horizontal"
data-stimeo--separator-focusable-value="true"
data-stimeo--separator-min-value="20"
data-stimeo--separator-max-value="80"
data-stimeo--separator-step-value="5"
data-stimeo--separator-value-value="50"
data-action="keydown->stimeo--separator#onKeydown"></div>
<%# Here the lower pane is the primary one. Flexing the last child from
--separator-split makes ArrowUp raise the primary pane's value while
moving the divider up. %>
<div class="separator-demo__pane" id="separator-demo-primary-stacked">
<%= t("components.separator.demo.bottom") %>
</div>
</div>
</div>
/*
* Presentation-only styles for the separator demo.
* The line's rendering, color, and thickness are owned by the consumer's CSS. The
* library only gives meaning to role / aria-orientation / aria-valuenow.
*/
.separator-demo {
max-width: 32rem;
}
.separator-demo__text {
margin: 0.5rem 0;
}
.separator--horizontal {
height: 1px;
margin: 0.75rem 0;
background: var(--border-strong);
}
.separator-demo__split {
display: flex;
align-items: stretch;
height: 6rem;
margin-top: 1rem;
border: 1px solid var(--border-default);
border-radius: 0.5rem;
overflow: hidden;
/* demo.js updates this variable from aria-valuenow (%) to set the left pane's width. */
--separator-split: 50%;
}
.separator-demo__pane {
display: grid;
place-items: center;
padding: 0.5rem;
background: var(--surface-subtle);
}
.separator-demo__pane:first-child {
flex: 0 0 var(--separator-split);
}
.separator-demo__pane:last-child {
flex: 1;
}
.separator--vertical {
flex: 0 0 0.5rem;
background: var(--border-strong);
cursor: col-resize;
}
/* Use :focus (not :focus-visible) so the color inversion shows on click too, not
only on keyboard focus — a resize handle is dragged with the mouse, and the user
needs to see it took focus. (Same reasoning as the time_picker demo.) */
.separator--vertical:focus {
outline: 2px solid var(--accent);
outline-offset: -2px;
background: var(--accent);
}
/* Horizontal, value-bearing separator: a top/bottom split. Stacks the panes vertically;
the value-to-pane mapping (the bottom pane) is handled by the rules just below. */
.separator-demo__split--stacked {
flex-direction: column;
height: 9rem;
}
/* Size the BOTTOM pane from the value so ArrowUp (increase) moves the handle up (the top
shrinks) — the handle follows the arrow direction instead of moving the opposite way. */
.separator-demo__split--stacked .separator-demo__pane:first-child {
flex: 1;
}
.separator-demo__split--stacked .separator-demo__pane:last-child {
flex: 0 0 var(--separator-split);
}
.separator--horizontal-handle {
flex: 0 0 0.5rem;
background: var(--border-strong);
cursor: row-resize;
}
.separator--horizontal-handle:focus {
outline: 2px solid var(--accent);
outline-offset: -2px;
background: var(--accent);
}
// Consumer-side JS for the separator demo (optional).
// A valued, focusable separator increments/decrements aria-valuenow with the arrow
// keys and fires stimeo--separator:change with { value }.
// Here we reflect that value (%) into the left pane's width to visualize the resize.
// The actual layout/size computation is the consumer's job (the library only gives the
// value meaning).
document.addEventListener("stimeo--separator:change", (event) => {
const split = event.target.closest(".separator-demo__split");
if (!split) return;
split.style.setProperty("--separator-split", `${event.detail.value}%`);
});
These demo styles use shared design tokens (light + dark). Copy the shared styles too, then toggle data-theme on your root element for dark mode.
The data-* attributes you add to your own HTML to wire this component. Put the data-controller below on a root element, then place its targets / values / actions inside that element.
On the root element
data-controller="stimeo--separator"
Values
| Name | Description | Attribute |
|---|---|---|
orientation
|
The rendered aria-orientation (horizontal default or vertical). |
data-stimeo--separator-orientation-value |
focusable
|
When true, makes the separator a value-bearing, focusable control with arrow-key adjustment (default false). | data-stimeo--separator-focusable-value |
min
|
The inclusive minimum rendered to aria-valuemin (default 0). |
data-stimeo--separator-min-value |
max
|
The inclusive maximum rendered to aria-valuemax (default 100). |
data-stimeo--separator-max-value |
step
|
The amount aria-valuenow changes per arrow-key press (default 1). |
data-stimeo--separator-step-value |
value
|
The current value rendered to aria-valuenow (default 0). |
data-stimeo--separator-value-value |
Actions
| Name | Description | Action |
|---|---|---|
onKeydown
|
Adjusts aria-valuenow via arrow keys and Home/End on the focusable variant, clamped to the range. |
stimeo--separator#onKeydown |
Events
| Name | Description | Event |
|---|---|---|
change
|
Dispatched when an arrow/Home/End key changes the value, with the clamped { value } in detail. |
stimeo--separator:change |
State hooks
The library only manages these ARIA/data attributes and custom properties. Your CSS reads them to render the look — selectors like [aria-selected], [aria-expanded], or var(--stimeo--…) hook into this state.
| Hook | Target | Meaning |
|---|---|---|
aria-orientation |
Root element | "horizontal" / "vertical". |
aria-valuenow |
Root element | The current value of a value-bearing separator (focusable). |