Install

Add Stimeo UI to your app

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-ui in config/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