タイマー / カウントダウン
stimeo--countdown
目標時刻までの残り時間を日/時/分/秒スロットに刻み、一時停止 / 再開できる。
stimeo--countdown コントローラは role="timer" のライブリージョンの慣行に従います。deadline までの残り時間(direction="up" では経過時間)を算定し、日/時/分/秒のスロットへ差し込み、 interval ごとに更新します。aria-live="off" で毎秒の読み上げを避け、完了のみを通知します(completeLabel を設定すると任意の status ライブリージョンへ、または complete イベントで)。一時停止 / 再開は内部の時間アンカーをずらすため、停止前の表示が保たれます。 stimeo--countdown:tick / :complete を発火し、インターバルは disconnect(Turbo 遷移含む)で破棄されます。ライブラリは挙動のみを提供し、スロットの見た目はこの Playground が持ちます。
<%# Markup for the countdown (timer) demo.
Fills the days/hours/minutes/seconds slots with the time remaining until the
deadline, updating every interval (default 1000ms). role="timer" + aria-live="off"
avoids per-second announcements; completion is announced by writing text into
status (aria-live="polite"). The deadline is computed in ERB to always be in the future.
The pause/resume/reset buttons sit outside the timer (a role="timer" live region
should not wrap unrelated controls), so they drive it through the countdown:* events
the controller listens for (see demo.js) instead of a Stimulus data-action, which
only binds within the controller's own element.
This demo behaves as a *duration* timer (a fixed length counted from "now"): the
deadline is now + the duration below, and Reset re-anchors it to now + the same
duration so the clock returns to the full time. The library only counts down to
whatever deadline it is given; demo.js owns that duration→deadline policy and exposes
the length via data-countdown-duration-ms. %>
<% countdown_duration = 1.hour %>
<div class="countdown-demo" data-countdown-duration-ms="<%= countdown_duration.to_i * 1000 %>">
<div
class="countdown"
data-controller="stimeo--countdown"
role="timer"
aria-live="off"
data-stimeo--countdown-deadline-value="<%= countdown_duration.from_now.iso8601 %>"
data-stimeo--countdown-complete-label-value="<%= t('components.countdown.demo.complete') %>"
data-action="
countdown:pause->stimeo--countdown#pause
countdown:resume->stimeo--countdown#resume
countdown:reset->stimeo--countdown#reset">
<span class="countdown__unit">
<span class="countdown__value" data-stimeo--countdown-target="days">0</span>
<span class="countdown__label"><%= t("components.countdown.demo.days") %></span>
</span>
<span class="countdown__unit">
<span class="countdown__value" data-stimeo--countdown-target="hours">00</span>
<span class="countdown__label"><%= t("components.countdown.demo.hours") %></span>
</span>
<span class="countdown__unit">
<span class="countdown__value" data-stimeo--countdown-target="minutes">00</span>
<span class="countdown__label"><%= t("components.countdown.demo.minutes") %></span>
</span>
<span class="countdown__unit">
<span class="countdown__value" data-stimeo--countdown-target="seconds">00</span>
<span class="countdown__label"><%= t("components.countdown.demo.seconds") %></span>
</span>
<span class="countdown__status" role="status" aria-live="polite"
data-stimeo--countdown-target="status"></span>
</div>
<div class="countdown-demo__controls">
<button class="demo-trigger" type="button" data-countdown-pause>
<%= t("components.countdown.demo.pause") %>
</button>
<button class="demo-trigger" type="button" data-countdown-resume>
<%= t("components.countdown.demo.resume") %>
</button>
<button class="demo-trigger" type="button" data-countdown-reset>
<%= t("components.countdown.demo.reset") %>
</button>
</div>
</div>
/*
* Presentation-only styles for the countdown demo.
* This CSS owns the slot styling; the library only updates each slot's text and
* reflects data-state (running / paused / complete).
*/
.countdown-demo {
display: flex;
flex-direction: column;
gap: 1rem;
max-width: 32rem;
}
.countdown {
display: flex;
align-items: flex-end;
gap: 0.75rem;
}
.countdown__unit {
display: inline-flex;
flex-direction: column;
align-items: center;
min-width: 3.25rem;
padding: 0.5rem;
border: 1px solid var(--border);
border-radius: 0.375rem;
background: var(--surface-subtle);
}
.countdown__value {
font-size: 1.6rem;
font-variant-numeric: tabular-nums;
font-weight: 600;
color: var(--fg);
}
.countdown__label {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--color-text-muted);
}
/* Visualize state: dimmed while paused, accent color when complete. */
.countdown[data-state="paused"] .countdown__value {
opacity: 0.5;
}
.countdown[data-state="complete"] .countdown__value {
color: var(--accent);
}
.countdown__status {
align-self: center;
font-size: 0.95rem;
color: var(--accent);
}
.countdown-demo__controls {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
// countdown external-control demo (consumer-side JS).
//
// The core controller (stimeo--countdown) ticks the timer and exposes pause /
// resume / reset, but owns no buttons. The role="timer" element is a live region
// that should not wrap unrelated controls, so the buttons live beside it — where a
// Stimulus data-action would never bind (actions only wire up within the
// controller's own element). The controller already listens for the countdown:*
// events (see the markup's data-action), so here each button dispatches the
// matching event on the timer element.
//
// Pause/Resume map straight to the controller. Reset is a duration-timer policy that
// lives here, not in the library: the controller counts down to a fixed deadline, so
// "reset to the full time" means re-anchoring the deadline to now + the configured
// duration before asking the controller to restart from it. Without this, reset would
// only re-sync to the original (already-elapsed) deadline and the clock would not
// return to the full time.
document.querySelectorAll(".countdown-demo").forEach((root) => {
const timer = root.querySelector('.countdown[data-controller~="stimeo--countdown"]');
const controls = root.querySelector(".countdown-demo__controls");
if (!timer || !controls) return;
const durationMs = Number(root.getAttribute("data-countdown-duration-ms")) || 0;
const wire = (selector, eventType) => {
controls.querySelector(selector)?.addEventListener("click", () => {
timer.dispatchEvent(new CustomEvent(eventType));
});
};
wire("[data-countdown-pause]", "countdown:pause");
wire("[data-countdown-resume]", "countdown:resume");
controls.querySelector("[data-countdown-reset]")?.addEventListener("click", () => {
if (durationMs > 0) {
const freshDeadline = new Date(Date.now() + durationMs).toISOString();
timer.setAttribute("data-stimeo--countdown-deadline-value", freshDeadline);
}
timer.dispatchEvent(new CustomEvent("countdown:reset"));
});
});
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。 共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--countdown"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
days
|
残り(経過)日数を表示するスロット要素。 | data-stimeo--countdown-target="days" |
hours
|
時間を2桁ゼロ埋めで表示するスロット要素。 | data-stimeo--countdown-target="hours" |
minutes
|
分を2桁ゼロ埋めで表示するスロット要素。 | data-stimeo--countdown-target="minutes" |
seconds
|
秒を2桁ゼロ埋めで表示するスロット要素。 | data-stimeo--countdown-target="seconds" |
status
|
完了時に完了ラベルを表示する任意のライブリージョン。 | data-stimeo--countdown-target="status" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
deadline
|
カウント対象の基準時刻(解析可能な日時文字列)。既定は空。 | data-stimeo--countdown-deadline-value |
interval
|
ティック間隔(ミリ秒、既定 1000)。 | data-stimeo--countdown-interval-value |
direction
|
カウント方向。down(残り)か up(経過)、既定は down。 |
data-stimeo--countdown-direction-value |
autostart
|
接続時に自動で開始するか(既定 true)。 | data-stimeo--countdown-autostart-value |
completeLabel
|
完了時に status へ書き込むテキスト。空なら無効。 | data-stimeo--countdown-complete-label-value |
アクション
| 名前 | 説明 | アクション |
|---|---|---|
pause
|
ティックを一時停止し、現在の表示量を保持する。 | stimeo--countdown#pause |
reset
|
期限へ再同期し一時停止のオフセットを解除する。実行状態は保持する。 | stimeo--countdown#reset |
resume
|
一時停止から、保持した量を引き継いで再開する。 | stimeo--countdown#resume |
start
|
期限へ向けてティックを開始(停止後は再開)する。 | stimeo--countdown#start |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
complete
|
カウントダウンが0に達したときに発火。 | stimeo--countdown:complete |
tick
|
毎ティックで発火。detail.remaining(表示量)と detail.direction を伴う。 | stimeo--countdown:tick |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
data-state |
ルート要素 | "running" / "paused" / "complete"。 |
テキスト |
日 / 時 / 分 / 秒スロット | 残り時間の各単位。 |