OTP 入力
stimeo--otp
確認コードを 1 桁ずつ入れる欄。貼り付けも自動入力も、桁ごとに振り分けます。
確認コードを桁ごとの欄に入れる入力です。桁数は並べた欄の数がそのまま使われるので、設定は要りません。1 文字入ると次の欄へ自動で進み、Backspace はその欄を消し、空なら前へ戻って消します。コードを丸ごと貼り付けたときや、携帯に届いたコードを OS が自動入力したとき、パスワード管理から入ってきたときは、桁ごとに振り分けます。日本語入力でまとめて確定した場合も同じです。受け付けない文字が来たときは、その欄の直前の文字に戻します。1 桁も埋まらなかったときだけ、そのことを知らせます。全部埋まったら、揃ったことを知らせます。
実行中
6桁の認証コードを入力してください
!
半角数字で入力してください。
キーボード操作
| キー | 動作 |
|---|---|
| Backspace | 入力済みの欄をその場で消去する。空欄の場合は前の欄へフォーカスを戻し、その値を消去する。 |
| ← | フォーカスを前の入力フィールドに移動する。RTL では → の動作。 |
| → | フォーカスを次の入力フィールドに移動する。RTL では ← の動作。 |
| Home | フォーカスを最初の入力フィールドに移動する。修飾キー付き(Control+Home)はブラウザへ渡す。 |
| End | フォーカスを最後の入力フィールドに移動する。修飾キー付き(Control+End)はブラウザへ渡す。 |
<%# Markup for the otp (OTP PIN passcode) demo.
Provides auto-advance on entry in each field, Backspace to clear, paste and
autofill splitting, and value sync. The digit count is the number of fields. %>
<div
class="otp-demo"
id="otp-passcode"
data-controller="stimeo--otp"
data-stimeo--otp-pattern-value="[0-9]"
role="group"
aria-label="<%= t("components.otp.demo.label") %>"
>
<div class="otp-demo__label"><%= t("components.otp.demo.label") %></div>
<div class="otp-demo__fields">
<% 6.times do |i| %>
<input
class="otp-demo__field"
type="text"
inputmode="numeric"
maxlength="1"
autocomplete="one-time-code"
data-stimeo--otp-target="field"
data-action="
input->stimeo--otp#onInput
keydown->stimeo--otp#onKeydown
paste->stimeo--otp#onPaste
pointerdown->stimeo--otp#onPointerDown
"
aria-label="<%= t("components.otp.demo.digit_label", position: i + 1) %>"
/>
<% end %>
</div>
<input
type="hidden"
data-stimeo--otp-target="value"
name="otp"
/>
<%# Region for invalid-input / full-width-character warning messages. %>
<div
data-stimeo--otp-target="error"
id="otp-error-message"
class="otp-demo__error"
role="alert"
aria-live="polite"
hidden
>
<span class="otp-demo__error-badge">!</span>
<span class="otp-demo__error-text">
<%= t("components.otp.demo.invalid_msg") %>
</span>
</div>
<%# The completion message is shown by CSS reading data-state on the root, so the
demo script only has to fill in the code itself. %>
<div id="otp-complete-message" class="otp-demo__message">
<span class="otp-demo__success-badge">✓</span>
<span class="otp-demo__success-text">
<%= t("components.otp.demo.complete_msg") %>
<strong id="otp-success-code"></strong>
</span>
</div>
<button type="button" class="demo-trigger otp-demo__clear" data-action="click->stimeo--otp#clear">
<%= t("components.otp.demo.clear") %>
</button>
</div>
/*
* Presentation-only styles for the otp demo.
* Whether a field is filled switches on data-filled, and whether the passcode is
* finished switches on data-state (both set by the library).
*/
.otp-demo {
display: inline-block;
font-family: inherit;
}
.otp-demo__label {
font-size: 0.875rem;
font-weight: 500;
color: var(--color-text-muted);
margin-bottom: 0.5rem;
}
.otp-demo__fields {
display: flex;
gap: 0.5rem;
}
.otp-demo__field {
width: 3rem;
height: 3.5rem;
text-align: center;
font-size: 1.5rem;
font-weight: 600;
color: var(--fg);
background: var(--surface-card);
border: 1px solid var(--border-strong);
border-radius: 8px;
outline: none;
transition: all 0.15s ease;
}
/* Filled state (data-filled). */
.otp-demo__field[data-filled="true"] {
border-color: var(--border-interactive);
}
/* Focus state. */
.otp-demo__field:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.15);
}
/* The completion message follows data-state="complete" on the group, so clearing a
digit hides it again without any script. */
.otp-demo__message {
display: none;
margin-top: 1rem;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
background: var(--leaf-50);
border: 1px solid var(--leaf-50);
border-radius: 8px;
animation: otpFadeIn 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}
.otp-demo[data-state="complete"] .otp-demo__message {
display: flex;
}
.otp-demo__clear {
margin-top: 1rem;
}
.otp-demo__success-badge {
display: flex;
align-items: center;
justify-content: center;
width: 1.25rem;
height: 1.25rem;
border-radius: 50%;
background: var(--leaf-500);
color: var(--white);
font-size: 0.75rem;
font-weight: bold;
}
.otp-demo__success-text {
font-size: 0.875rem;
color: var(--leaf-500);
}
.otp-demo__error {
margin-top: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
background: var(--danger-50);
border: 1px solid var(--ruby-200);
border-radius: 8px;
animation: otpFadeIn 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}
/* Stimeo controls visibility only via the hidden attribute (behavior only); the styling reads
that state. */
.otp-demo__error[hidden] {
display: none;
}
.otp-demo__error-badge {
display: flex;
align-items: center;
justify-content: center;
width: 1.25rem;
height: 1.25rem;
border-radius: 50%;
background: var(--danger-500);
color: var(--white);
font-size: 0.75rem;
font-weight: bold;
}
.otp-demo__error-text {
font-size: 0.875rem;
color: var(--color-accent);
}
@keyframes otpFadeIn {
from {
opacity: 0;
transform: translateY(4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
// Demo script that fills the celebration message with the completed passcode.
// Whether that message is shown is CSS reading data-state on the group, so the
// script never has to track emptying, correcting, or clearing the code.
document.getElementById('otp-passcode').addEventListener('stimeo--otp:complete', function (e) {
const code = document.getElementById('otp-success-code');
if (code) code.textContent = e.detail.value;
});
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--otp"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
field
必須
|
1文字ずつの入力欄。並べた数がそのまま桁数になり、フォーカスが自動で進む。 | data-stimeo--otp-target="field" |
value
|
連結したコードを保持する隠しフィールド。フォーム送信用。 | data-stimeo--otp-target="value" |
error
|
1欄も埋まらなかった入力を知らせるために表示・非表示を切り替えるエラー要素。 | data-stimeo--otp-target="error" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
pattern
|
1文字ごとの検証用正規表現(既定 [0-9])。解釈できない宣言は既定値へ落とす。 | data-stimeo--otp-pattern-value |
アクション
| 名前 | 説明 | アクション |
|---|---|---|
onInput
|
入力・自動入力された文字を検証し、受理した分を後続の欄へ分配してフォーカスを進める。 | stimeo--otp#onInput |
onKeydown
|
Backspace のクリア・後退と矢印・Home/End のナビを処理する。 | stimeo--otp#onKeydown |
onPaste
|
貼り付けを横取りし、有効な文字を各欄へ分配する。 | stimeo--otp#onPaste |
onPointerDown
|
空欄をポインタで押したとき、先頭の空欄へ着地させて順番に埋まるようにする。 | stimeo--otp#onPointerDown |
clear
|
書き込み可能な全欄を空にして先頭へフォーカスを戻す。「やり直す」ボタン用。 | stimeo--otp#clear |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
change
|
連結値が実際に変わったときに発火。detail にその値を載せる。 | stimeo--otp:change |
complete
|
全欄が埋まった状態へ連結値が変わったときに発火。detail に完成値を載せる。 | stimeo--otp:complete |
invalid
|
入力が 1 欄も埋められずに破棄されたときに発火。detail に実際に適用された pattern を載せる。 | stimeo--otp:invalid |
reconcile
|
欄の増減で結合値が動いたときに発火。再描画を編集と読んだ自動化が送信を繰り返さないようにする。 | stimeo--otp:reconcile |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
data-filled |
個々の入力フィールド (field) | 文字が入力されている場合に "true"、空の場合には属性自体を削除。 |
data-state |
コントローラ要素 | "empty"(全欄空)/ "partial"(一部入力)/ "complete"(全欄入力)を公開する。 |
aria-invalid / aria-errormessage |
個々の入力フィールド (field) | 破棄された入力を報告している間だけ付与し、入力が受理されると利用側が書いた値へ返す。 |
hidden |
エラー要素 (error) | 破棄された入力を報告している間だけ外れる。解消すると利用側が書いた表示状態へ戻る。 |
value |
隠し input 要素 (value) | 各フィールドの値を連結した最終的な PIN 文字列が自動同期される。 |