Dismissible
stimeo--dismissible
Closes a notice or banner without leaving focus stranded when the button disappears with it.
Closes a notice or banner, either removing it or hiding it, and can answer to Esc as well. When focus is inside what is about to vanish, it moves on to the destination you named, then to the next control, then the previous one, so the close button taking itself away never strands the keyboard. Closing is reported so you can carry on from it. Enter and exit animations, and marking it up as an alert, stay with you.
Keyboard
| Key | Action |
|---|---|
| Enter / Space | Activate the close button (native button). |
| Escape | Dismiss when closeOnEscape is set and focus is inside. |
<%# Markup for the dismissible (element dismissed via a close button) demo.
The close button (or Escape) removes the element, and if focus was inside before
removal it is moved to a safe place (fallback → next/previous focusable → body).
Here it uses hide mode (adds hidden) and allows closing with Escape too.
Semantics like role="status" are the consumer markup's responsibility. %>
<div
class="dismissible"
data-controller="stimeo--dismissible"
data-stimeo--dismissible-mode-value="hide"
data-stimeo--dismissible-close-on-escape-value="true">
<div
class="dismissible__banner"
role="status"
data-stimeo--dismissible-target="root">
<span class="dismissible__icon" aria-hidden="true">✓</span>
<p class="dismissible__message"><%= t("components.dismissible.demo.message") %></p>
<button
class="dismissible__close"
type="button"
aria-label="<%= t('components.dismissible.demo.close') %>"
data-action="click->stimeo--dismissible#dismiss">
<span aria-hidden="true">×</span>
</button>
</div>
<a class="dismissible__after" href="#" data-stimeo--dismissible-target="fallback">
<%= t("components.dismissible.demo.fallback") %>
</a>
</div>
/*
* Presentation-only styles for the dismissible demo.
* Dismissal is expressed via hide mode (the library adds hidden to the root).
*/
.dismissible {
display: flex;
flex-direction: column;
gap: 0.5rem;
max-width: 32rem;
}
.dismissible__banner {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
border: 1px solid var(--leaf-50);
border-radius: 0.5rem;
background: var(--leaf-50);
/* leaf-500 is the status *fill*; carrying text it measures 3.11:1 on this
tinted surface. The 700 step exists for exactly this (status TEXT). */
color: var(--leaf-700);
}
.dismissible__icon {
font-weight: 700;
}
.dismissible__message {
margin: 0;
flex: 1;
}
.dismissible__close {
display: grid;
place-items: center;
width: 1.75rem;
height: 1.75rem;
border: 0;
border-radius: 0.25rem;
background: transparent;
color: inherit;
font-size: 1.25rem;
line-height: 1;
cursor: pointer;
}
.dismissible__close:hover {
background: var(--leaf-50);
}
.dismissible__close:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.dismissible__after {
align-self: flex-start;
font-size: 0.875rem;
color: var(--accent-700);
}
This demo needs no consumer-side JS (the controller handles the behavior).
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--dismissible"
Targets
| Name | Description | Attribute |
|---|---|---|
root
|
The element to dismiss; falls back to the host element when omitted. | data-stimeo--dismissible-target="root" |
fallback
|
The preferred element to receive focus when dismissal would orphan focus inside root. | data-stimeo--dismissible-target="fallback" |
Values
| Name | Description | Attribute |
|---|---|---|
mode
|
How to dismiss: remove removes the element from the DOM (default) or hide sets it hidden. |
data-stimeo--dismissible-mode-value |
closeOnEscape
|
When true, pressing Escape while focus is inside dismisses the element (default false). | data-stimeo--dismissible-close-on-escape-value |
Actions
| Name | Description | Action |
|---|---|---|
dismiss
|
Retreats focus if needed, then removes or hides the element per mode. |
stimeo--dismissible#dismiss |
Events
| Name | Description | Event |
|---|---|---|
dismiss
|
Dispatched synchronously just before removal/hiding, with the resolved { mode } in detail. |
stimeo--dismissible:dismiss |
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 |
|---|---|---|
hidden |
Root | Added on dismiss in hide mode (remove mode deletes the node). |
data-state |
Root | "open" / "closing" — optional exit-animation starting point. |