Smart Sticky Header
stimeo--smart-sticky-header
The header tucks away as you read down and returns as you scroll up, and always for the keyboard.
Tucks the header away while you read downwards and brings it back when you scroll up, giving more room and returning the moment you want it. A little tolerance keeps a jittery scroll from flapping it. Tabbing into the header always reveals it, so a keyboard user can see where focus went. It watches the page by default, and in a layout where only an inner box scrolls you can point it there. How it tucks away, and what happens for visitors who ask for less motion, are your CSS.
Scroll down inside the frame to tuck the bar away, and scroll up — or Tab into it — to bring it back.
Keyboard
This component has no keyboard interactions of its own.
<%# smart-sticky-header: the library only flips data-header-hidden from the scroll
direction (and reveals on focus); the slide-away is this demo's CSS transform.
Scroll down inside the frame to hide the bar, up (or Tab into it) to reveal. %>
<div class="smart-sticky-demo">
<p class="smart-sticky-demo__hint"><%= t("components.smart_sticky_header.demo.hint") %></p>
<div id="smart-sticky-viewport" class="smart-sticky-demo__viewport" role="region" tabindex="0"
aria-label="<%= t('components.smart_sticky_header.demo.viewport_label') %>">
<header class="smart-sticky-demo__header" data-controller="stimeo--smart-sticky-header"
data-stimeo--smart-sticky-header-container-selector-value="#smart-sticky-viewport"
data-stimeo--smart-sticky-header-offset-value="40">
<a href="#smart-sticky-demo-content"><%= t("components.smart_sticky_header.demo.brand") %></a>
</header>
<div id="smart-sticky-demo-content" class="smart-sticky-demo__content">
<% 10.times do |i| %>
<p><%= t("components.smart_sticky_header.demo.paragraph", index: i + 1) %></p>
<% end %>
</div>
</div>
</div>
/*
* Presentation-only styles for the smart-sticky-header demo. The library flips
* data-header-hidden; the slide-away transform (and its reduced-motion opt-out)
* lives here. The controller is pointed at the demo's own scroll frame via
* containerSelector (the catalog page itself does not scroll).
*/
.smart-sticky-demo__hint {
margin: 0 0 0.75rem;
color: var(--muted);
}
.smart-sticky-demo__viewport {
position: relative;
height: 16rem;
overflow-y: auto;
border: 1px solid var(--border);
border-radius: 0.5rem;
}
.smart-sticky-demo__viewport:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.smart-sticky-demo__header {
position: sticky;
top: 0;
padding: 0.75rem 1rem;
background: var(--bg);
border-bottom: 1px solid var(--border);
transition: transform 0.2s ease;
}
.smart-sticky-demo__header[data-header-hidden="true"] {
transform: translateY(-100%);
}
@media (prefers-reduced-motion: reduce) {
.smart-sticky-demo__header {
transition: none;
}
}
.smart-sticky-demo__content {
padding: 1rem;
}
.smart-sticky-demo__content p {
margin: 0 0 1.25rem;
color: var(--fg);
}
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--smart-sticky-header"
Values
| Name | Description | Attribute |
|---|---|---|
containerSelector
|
Selector for the scroll source; empty (default) means the window. | data-stimeo--smart-sticky-header-container-selector-value |
offset
|
Never hide within this many px of the top. Default 80. | data-stimeo--smart-sticky-header-offset-value |
tolerance
|
Ignore scroll deltas below this (jitter guard). Default 4. | data-stimeo--smart-sticky-header-tolerance-value |
Events
| Name | Description | Event |
|---|---|---|
change
|
Fires with { hidden } on state transitions only. |
stimeo--smart-sticky-header: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 |
|---|---|---|
data-header-hidden |
Controller element | "true" (tucked away) / "false" (shown) — the consumer's CSS translates it off-screen. |