Stimeo UI は Rails 向けに 挙動だけ を提供する Stimulus コントローラ群です。
ARIA 状態、キーボード操作、フォーカス管理を data-* 属性で駆動し、CSS は一切同梱しません。見た目は完全にあなたのものです。
ステータス: プレリリース版(0.x)です。
stimeo--*の属性 API は 1.0 までに変わる可能性があります。
Rails + importmap(推奨)
bundle add stimeo-ui
bin/rails generate stimeo:install
このコマンド(stimeo:install ジェネレータ)は、次のセットアップを自動で行います:
- ビルド済みのライブラリ本体を
vendor/javascript/stimeo/に配置(同梱)し、 config/importmap.rbにstimeo-uiの読み込み設定(importmap のエントリ)を追加し、- コアのコントローラを Stimulus に登録し(
app/javascript/controllers/stimeo.js)、 - オプトインの positioning と cable を、この 2 つのファイルにコメントアウトした形で書き込みます。使うものの行のコメントを外して有効にします。positioning には、その節に載せた
@floating-ui/domの pin も要ります。この行はジェネレータが書きません(下の各節を参照)。
何度実行しても安全です。ライブラリ本体は最新版に置き換わり、既にある stimeo-ui の読み込み設定と controllers/stimeo.js はそのまま残ります。そのため、再実行しても既存の構成にオプトインの行は足されません。必要な行は下の各節に載せています。
Inspector は npm 側で配布しています。
stimeoインスペクタ CLI は Node 製のツールのため、gem には同梱されていません。gem で導入したアプリでも、プロジェクトに npm を追加せずにnpx stimeo check app/viewsでその場で取得・実行できます。必要なのは Node の実行環境だけで、GitHub Actions の GitHub ホステッドランナーには最初から入っています。
あとは HTML に属性を書くだけでコンポーネントが動きます:
<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 など任意のバンドラ)
npm install stimeo-ui @hotwired/stimulus
import { Application } from "@hotwired/stimulus";
import { registerStimeo } from "stimeo-ui";
const application = Application.start();
registerStimeo(application); // コアのエントリポイントのコントローラをすべて登録
オプトインの stimeo-ui/positioning と stimeo-ui/cable は、それぞれ
registerPositioning / registerCable で自分のコントローラを登録します(下記)。
必要なコントローラだけを使いたい場合は、stimeo-ui/controllers/* から個別に
import して、好きな識別子で登録することもできます:
import { Application } from "@hotwired/stimulus";
import { DialogController } from "stimeo-ui/controllers/dialog_controller";
const application = Application.start();
application.register("stimeo--dialog", DialogController);
オプトインの positioning(フローティング配置)
popover / tooltip / hover-card のような「何かに沿って浮かぶ UI」では、開いたり閉じたりといった動きはコントローラが担います。一方でどこに表示するか(位置)は、通常はあなたの CSS で決めます。画面の端に来たときに自動で向きを反転させて収める、といった「動的な位置合わせ」が欲しいときだけ、@floating-ui/dom を使う positioning モジュール(stimeo-ui/positioning)を任意で追加します。必要なければ追加は不要で、本体(コアの stimeo-ui)には positioning モジュールも @floating-ui/dom も含まれないため、追加の依存はゼロのままです。使う場合は config/importmap.rb に次の 2 行を足して、サブパスと依存ライブラリも読み込めるようにします(1 行目はジェネレータがコメントアウトした形で書き込んでいます):
pin "stimeo-ui/positioning", to: "stimeo/positioning/index.js"
pin "@floating-ui/dom", to: "floating-ui-dom.js"
宣言的に位置を合わせる stimeo--anchored コントローラを使うときは、このモジュールのコントローラも登録します(この 2 行もジェネレータが controllers/stimeo.js にコメントアウトした形で書き込んでいます):
import { registerPositioning } from "stimeo-ui/positioning";
registerPositioning(application);
オプトインの cable(リアルタイム)
ライブカウンター、在席表示、入力中の表示は、状態をページの中ではなく Action Cable のチャンネルに持ちます。これらのコントローラ(stimeo--live-counter /
stimeo--presence / stimeo--typing-indicator)は、@rails/actioncable を使うオプトインの cable モジュール(stimeo-ui/cable)に入っています。本体(コアの
stimeo-ui)にはモジュールも @rails/actioncable も含まれないため、使うときだけ追加します。importmap では、サブパスと依存ライブラリを pin します(2 行ともジェネレータがコメントアウトした形で書き込んでいます):
pin "stimeo-ui/cable", to: "stimeo/cable/index.js"
pin "@rails/actioncable", to: "actioncable.esm.js"
そのうえで、コアのコントローラと並べて登録します(registerCable の行もジェネレータが
controllers/stimeo.js にコメントアウトした形で書き込んでいます)。アプリが既に
Action Cable の consumer を持っているなら、先に setCableConsumer で渡すと
WebSocket を 1 本で共有できます。持っていなければ、最初に使うときにモジュールが自分で作ります。
import consumer from "channels/consumer"; // アプリが consumer を持つ場合だけ
import { registerCable, setCableConsumer } from "stimeo-ui/cable";
setCableConsumer(consumer);
registerCable(application);
Lint(リント)
Stimeo UI はヘッドレス(見た目を持たない)なので、role などの WAI-ARIA 属性は
あなた自身が書きます。一部のコントローラは、その属性を手がかりに要素を探します(例: data-grid は [role="row"] で行を見つけます)。そのため、あなたのマークアップには
<ul role="menu"> / <div role="radio"> / <td role="gridcell"> のような、APG に沿った正しいカスタムウィジェットの ARIA が含まれます。
ところが厳格な静的 a11y リンタ(Biome の recommended プリセット(2.5 以降)や
eslint-plugin-jsx-a11y)は、こうした正しい書き方をエラー扱いします。<button> のようなネイティブ要素を前提にしたチェックだからです。そこで Stimeo UI のマークアップを書く場所だけ 該当ルールを緩めます(includes のパスは自分の構成に合わせて調整します)。
Biome なら biome.json に次の overrides を追加します:
{
"overrides": [
{
"includes": ["app/components/**"],
"linter": {
"rules": {
"a11y": {
"noNoninteractiveElementToInteractiveRole": "off",
"noRedundantRoles": "off",
"useSemanticElements": "off",
"useFocusableInteractive": "off",
"noNoninteractiveTabindex": "off"
}
}
}
}
]
}
ESLint(eslint-plugin-jsx-a11y)を使う場合も、同じ考え方で、対象のパス(.eslintrc.json など)だけ同等のルールを off にします。
次のステップ
- スタイリングの注意点を読む —— 部品がどう出し隠しするか、状態で変わる文言をどこに置くか。
- コンポーネントカタログでライブデモを見る。
- デモが前提とする共通スタイルをコピーする。
- Inspector(
stimeo check)でマークアップを検査する。