Transition
stimeo--transition
Stages enter/leave CSS classes for showing and hiding an element, settling on transitionend or a safety timeout.
The stimeo--transition controller is the shared substrate for show/hide animations — the counterpart to Headless UI Transition / Alpine x-transition. enter() unhides the element, applies the enter and enterFrom classes, then on the next frame swaps enterFrom → enterTo so the CSS transition runs, and on transitionend (or a safety timeout derived from timeout or the computed duration) settles to the entered state. leave() mirrors it and re-applies hidden; toggle() reverses the current direction. The element carries data-transition-state (entering / entered / leaving / left) and emits entered / left on completion. Behavior only — the animation itself is the consumer's CSS, this just controls when the stage classes are applied. Under prefers-reduced-motion it switches instantly (no staging), an interrupting call cancels the in-flight transition, and connect reconciles to a stable state (stripping any half-applied stage classes from a Turbo cache). The transitionend listener, rAF, and safety timer are released on disconnect (Turbo navigation included).
<%# Transition demo: the panel is the controller element; enter/leave class values point
at the demo.css classes that define the timing and start/end states. The toggle button
lives outside the (hideable) panel, so it invokes the controller's toggle action through
the playground's exposed Stimulus app (window.Stimulus) in demo.js. The library only
stages the classes and reflects data-transition-state / hidden; demo.css owns the look. %>
<div class="transition-demo">
<button
type="button"
class="demo-trigger"
data-transition-demo-toggle
aria-expanded="false"
aria-controls="transition-demo-panel">
<%= t("components.transition.demo.toggle") %>
</button>
<div
id="transition-demo-panel"
class="transition-demo__panel"
data-controller="stimeo--transition"
data-stimeo--transition-enter-value="te-enter"
data-stimeo--transition-enter-from-value="te-from"
data-stimeo--transition-enter-to-value="te-to"
data-stimeo--transition-leave-value="te-leave"
data-stimeo--transition-leave-from-value="te-to"
data-stimeo--transition-leave-to-value="te-from"
hidden>
<%= t("components.transition.demo.panel") %>
</div>
</div>
/*
* Presentation-only styles for the transition demo. The library stages the te-* classes
* (named by the controller's enter/leave values) and toggles hidden; these classes define
* the timing and the start/end states. After a transition completes the controller strips
* the stage classes, so the panel rests at the te-to (visible) state by default.
*/
.transition-demo {
display: flex;
flex-direction: column;
gap: 0.75rem;
align-items: flex-start;
}
.transition-demo__panel {
max-width: 22rem;
padding: 1rem;
border: 1px solid var(--border);
border-radius: 0.5rem;
background: var(--surface-subtle);
}
/* Stage classes referenced by the controller's enter/leave value attributes. */
.te-enter {
transition:
opacity 0.3s ease-out,
transform 0.3s ease-out;
}
.te-leave {
transition:
opacity 0.2s ease-in,
transform 0.2s ease-in;
}
.te-from {
opacity: 0;
transform: translateY(-0.5rem);
}
.te-to {
opacity: 1;
transform: translateY(0);
}
@media (prefers-reduced-motion: reduce) {
.te-enter,
.te-leave {
transition: none;
}
}
// Transition demo (consumer-side JS).
//
// The toggle button sits outside the hideable panel, so a plain data-action can't reach
// the panel's controller. The playground exposes its Stimulus application as
// window.Stimulus, so we fetch the controller instance and call its toggle() action — and
// mirror the open state onto aria-expanded from the controller's entered/left events.
document.querySelectorAll(".transition-demo").forEach((root) => {
const panel = root.querySelector('[data-controller~="stimeo--transition"]');
const button = root.querySelector("[data-transition-demo-toggle]");
if (!panel || !button) return;
// Idempotent: Turbo can re-run this inline module on navigation; wire each root once so a
// single click does not toggle twice (which flashes the panel open then shut).
if (root.dataset.demoWired) return;
root.dataset.demoWired = "1";
button.addEventListener("click", () => {
window.Stimulus?.getControllerForElementAndIdentifier(panel, "stimeo--transition")?.toggle();
});
panel.addEventListener("stimeo--transition:entered", () => {
button.setAttribute("aria-expanded", "true");
});
panel.addEventListener("stimeo--transition:left", () => {
button.setAttribute("aria-expanded", "false");
});
});
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--transition"
Values
| Name | Description | Attribute |
|---|---|---|
enter
|
Classes applied for the whole enter transition (e.g. timing / easing). | data-stimeo--transition-enter-value |
enterFrom
|
Enter start-state classes (applied first, removed next frame). | data-stimeo--transition-enter-from-value |
enterTo
|
Enter end-state classes (applied next frame so the transition runs). | data-stimeo--transition-enter-to-value |
leave
|
Classes applied for the whole leave transition. | data-stimeo--transition-leave-value |
leaveFrom
|
Leave start-state classes. | data-stimeo--transition-leave-from-value |
leaveTo
|
Leave end-state classes. | data-stimeo--transition-leave-to-value |
timeout
|
Safety completion timeout in ms (0 = auto from the computed duration). | data-stimeo--transition-timeout-value |
Actions
| Name | Action |
|---|---|
enter
|
stimeo--transition#enter |
leave
|
stimeo--transition#leave |
toggle
|
stimeo--transition#toggle |
Events
| Name | Description | Event |
|---|---|---|
entered
|
Fires when the enter transition completes. | stimeo--transition:entered |
left
|
Fires when the leave transition completes. | stimeo--transition:left |
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 |
|---|---|---|
data-transition-state |
Controller element | entering / entered / leaving / left. |
hidden |
Controller element | Removed when entering starts; re-applied when leaving completes. |