Anchored Positioning
stimeo--anchored
Positions a floating element against an anchor, flipping/shifting it away from edges.
The stimeo--anchored controller is the declarative surface of the opt-in stimeo-ui/positioning engine (floating-ui based) — it keeps a floating element placed against an anchor and flips/shifts it away from viewport edges as the page scrolls or resizes (floating-ui autoUpdate as a controller). It writes only position/left/top inline styles — never decoration — and mirrors the resolved (post-flip) side onto data-anchored-placement on the floating element for CSS hooks such as an arrow. active drives tracking: set it false while the floating element is hidden so no measurement runs; the other values map to the engine and re-apply live. It emits position on each update. Behavior only — it does not open/close, manage focus, or move DOM (pair with Dialog/Popover, Focus Scope, and Portal). It ships in the opt-in stimeo-ui/positioning subpath, so the core import stays zero-dependency; only consumers who register it pull in floating-ui — this demo's JS imports the subpath and registers stimeo--anchored.
The panel is positioned against the anchor. Choose a side; the panel shows the resolved placement (flip/shift may change it near an edge).
<%# Anchored positioning demo: the panel is positioned against the anchor button by the
opt-in stimeo--anchored controller (it writes only position/left/top). Choose a side
with the buttons — demo.js sets the placement value — and the panel mirrors the
resolved side via data-anchored-placement (shown through demo.css). The controller is
registered from the opt-in stimeo-ui/positioning subpath in demo.js; demo.css owns the
look. %>
<div class="anchored-demo">
<p class="anchored-demo__hint"><%= t("components.anchored.demo.hint") %></p>
<div class="anchored-demo__controls" role="group"
aria-label="<%= t('components.anchored.demo.placement_label') %>">
<button type="button" class="demo-trigger" data-placement="top" aria-pressed="false">
<%= t("components.anchored.demo.placements.top") %>
</button>
<button type="button" class="demo-trigger" data-placement="right" aria-pressed="false">
<%= t("components.anchored.demo.placements.right") %>
</button>
<button type="button" class="demo-trigger" data-placement="bottom" aria-pressed="true">
<%= t("components.anchored.demo.placements.bottom") %>
</button>
<button type="button" class="demo-trigger" data-placement="left" aria-pressed="false">
<%= t("components.anchored.demo.placements.left") %>
</button>
</div>
<div class="anchored-demo__viewport">
<div class="anchored-demo__scope" data-controller="stimeo--anchored"
data-stimeo--anchored-placement-value="bottom"
data-stimeo--anchored-offset-value="8"
data-stimeo--anchored-padding-value="8">
<button type="button" class="demo-trigger anchored-demo__anchor"
data-stimeo--anchored-target="anchor">
<%= t("components.anchored.demo.anchor") %>
</button>
<div class="anchored-demo__floating" data-stimeo--anchored-target="floating"></div>
</div>
</div>
</div>
/*
* Presentation-only styles for the anchored-positioning demo. The library writes only
* position/left/top on the floating element and mirrors the resolved side onto
* data-anchored-placement; this CSS frames the viewport (a positioned ancestor the
* absolute coordinates resolve against), the anchor, and the floating panel — and shows
* the resolved placement through the data-* hook.
*/
.anchored-demo {
display: flex;
flex-direction: column;
gap: 0.75rem;
align-items: flex-start;
width: 100%;
}
.anchored-demo__hint {
margin: 0;
color: var(--muted);
}
.anchored-demo__controls {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
/* Positioned ancestor: the engine's absolute coordinates resolve against this box. */
.anchored-demo__viewport {
position: relative;
display: grid;
place-items: center;
width: 100%;
min-height: 16rem;
border: 1px dashed var(--border);
border-radius: 0.5rem;
}
/* The scope is layout-transparent so the anchor centers in the viewport grid and the
floating element positions against the viewport, not an extra box. */
.anchored-demo__scope {
display: contents;
}
.anchored-demo__floating {
/* The engine sets position/left/top; everything here is look-only and themable.
Start absolute so the panel never disrupts layout before the controller connects. */
position: absolute;
top: 0;
left: 0;
min-width: 7rem;
padding: 0.5rem 0.75rem;
border: 1px solid var(--accent);
border-radius: 0.375rem;
background: var(--bg);
color: var(--fg);
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
}
/* Surface the resolved (post-flip) placement from the controller's data-* hook. */
.anchored-demo__floating::after {
content: "placement: " attr(data-anchored-placement);
color: var(--muted);
}
// anchored opt-in positioning demo (consumer-side JS).
//
// stimeo--anchored ships in the opt-in stimeo-ui/positioning subpath, so it is NOT
// auto-registered by registerStimeo (which keeps the core install zero-dependency).
// Importing the subpath here is what pulls in @floating-ui/dom; we register the
// controller on the playground's Stimulus app, then wire the placement buttons to the
// controller's placement value so picking a side re-positions the panel.
import { AnchoredController } from "stimeo-ui/positioning";
// Register once (Turbo can re-run this inline module on navigation).
if (window.Stimulus && !window.__stimeoAnchoredRegistered) {
window.Stimulus.register("stimeo--anchored", AnchoredController);
window.__stimeoAnchoredRegistered = true;
}
document.querySelectorAll(".anchored-demo").forEach((root) => {
// Idempotent: wire each root once even if this module re-runs.
if (root.dataset.demoWired) return;
root.dataset.demoWired = "1";
const scope = root.querySelector('[data-controller~="stimeo--anchored"]');
if (!scope) return;
const buttons = root.querySelectorAll("[data-placement]");
buttons.forEach((button) => {
button.addEventListener("click", () => {
// Setting the value triggers placementValueChanged → the controller re-positions.
scope.setAttribute("data-stimeo--anchored-placement-value", button.dataset.placement);
buttons.forEach((other) => other.setAttribute("aria-pressed", String(other === button)));
});
});
});
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--anchored"
Targets
| Name | Description | Attribute |
|---|---|---|
anchor
required
|
The reference element the floating element is positioned against. | data-stimeo--anchored-target="anchor" |
floating
required
|
The element that is positioned (only its coordinates are written). | data-stimeo--anchored-target="floating" |
Values
| Name | Description | Attribute |
|---|---|---|
placement
|
Preferred side (floating-ui Placement, e.g. top-start); default bottom. |
data-stimeo--anchored-placement-value |
offset
|
Gap in px between the anchor and the floating element; default 0. | data-stimeo--anchored-offset-value |
flip
|
Flip to the opposite side when the preferred side would overflow; default true. | data-stimeo--anchored-flip-value |
shift
|
Shift along the axis to keep it in view; default true. | data-stimeo--anchored-shift-value |
padding
|
Padding (px) kept from the viewport edge when flipping/shifting; default 0. | data-stimeo--anchored-padding-value |
strategy
|
CSS positioning strategy, absolute or fixed; default absolute. |
data-stimeo--anchored-strategy-value |
active
|
Whether tracking is on; set false while hidden to stop measuring; default true. | data-stimeo--anchored-active-value |
Events
| Name | Description | Event |
|---|---|---|
position
|
Fires with { placement, x, y } each time the position is computed. | stimeo--anchored:position |
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-anchored-placement |
Floating target | The resolved placement after flip/shift (e.g. top-start); a CSS hook for arrows. |
position / left / top |
Floating target | Inline styles written on each update; no decoration is ever set. |