Stimeo UI ships behavior-only Stimulus controllers for Rails — ARIA state,
keyboard, and focus driven by data-* attributes. It ships no CSS, so the look
stays entirely yours.
Status: alpha (0.x). The
stimeo--*attribute API may still change before 1.0 — pin your version.
Rails with importmap (recommended)
bundle add stimeo-ui
bin/rails generate stimeo:install
The generator:
- vendors the prebuilt JS into
vendor/javascript/stimeo/, - pins
stimeo-uiinconfig/importmap.rb, - registers every
stimeo--*controller with your Stimulus application.
Re-running it is safe: the vendored copy is refreshed in place while existing pins are left untouched.
Then drive components from HTML alone:
<div data-controller="stimeo--dropdown">
<button data-stimeo--dropdown-target="trigger"
data-action="click->stimeo--dropdown#toggle">Menu</button>
<div data-stimeo--dropdown-target="menu" hidden>…</div>
</div>
npm (jsbundling or any bundler)
npm install stimeo-ui @hotwired/stimulus
import { Application } from "@hotwired/stimulus";
import { registerStimeo } from "stimeo-ui";
const application = Application.start();
registerStimeo(application); // registers every stimeo--* controller
Prefer cherry-picking? Import controllers individually from
stimeo-ui/controllers/* and register them under your own identifiers.
Opt-in positioning
For floating overlays like popover, tooltip, and hover-card, the controller
handles the behavior (open / close), while where the panel appears is normally
up to your CSS. Only when you want dynamic placement — following the anchor and
flipping to stay on-screen at viewport edges — add the opt-in positioning module
(stimeo-ui/positioning), backed by @floating-ui/dom. You don't have to: the
core stimeo-ui entry includes neither the module nor @floating-ui/dom, so
your bundle stays zero-extra-dependency unless you opt in. With importmap, also pin the subpath and its engine:
pin "stimeo-ui/positioning", to: "stimeo/positioning/index.js"
pin "@floating-ui/dom", to: "floating-ui-dom.js"
Linting
Stimeo UI is headless, so you author the WAI-ARIA roles yourself — and some
controllers read them back as selector contracts (the data-grid finds its rows
via [role="row"]). Your markup therefore holds valid custom-widget ARIA like
<ul role="menu">, <div role="radio">, and <td role="gridcell">.
Strict static a11y linters — Biome's recommended preset (≥ 2.5) and
eslint-plugin-jsx-a11y — flag these valid APG patterns, because their
heuristics assume native semantic elements. Relax the conflicting rules only for
the paths where you author Stimeo UI markup (adjust includes to your layout).
In Biome, add these overrides to biome.json:
{
"overrides": [
{
"includes": ["app/components/**"],
"linter": {
"rules": {
"a11y": {
"noNoninteractiveElementToInteractiveRole": "off",
"noRedundantRoles": "off",
"useSemanticElements": "off",
"useFocusableInteractive": "off",
"noNoninteractiveTabindex": "off"
}
}
}
}
]
}
If you use ESLint (eslint-plugin-jsx-a11y) instead, turn off the equivalent
rules for the same paths (in your .eslintrc.json or equivalent).
Next steps
- Browse the live demos in the component catalog.
- Copy the shared styles the demos build on.
- Check your markup with the Inspector (
stimeo check).