NumberInput
数値の入力に役立つ機能を持った入力欄。
Source Style- React
- Svelte
- Vue
function App() { const [value, setValue] = useState(64) return ( <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', }} > <NumberInput.Root className={numberInputTheme.root} value={value} min={0} max={100} onChange={(v) => setValue(v)} > <NumberInput.InputField className={numberInputTheme.field} aria-label="Value" /> <NumberInput.Stepper className={numberInputTheme.stepper}> <NumberInput.IncrementStepper className={numberInputTheme.incrementStepper} /> <NumberInput.DecrementStepper className={numberInputTheme.decrementStepper} /> </NumberInput.Stepper> </NumberInput.Root> </div> ) }
/** The demo theme for `NumberInput`, as a CSS Module: one class per part, applied by* the caller. Copy this file into your project and edit it.*/.root {--border-radius: 4px;--stepper-width: 20px;--stepper-icon-size: 12px;--active-color: oklch(59.5% 0.17548 266.373);--border-color: oklch(81% 0 0);/* Room for the padding, the stepper, and a few digits. A flex row that runsshort shrinks its items, and the field inside is sized from the box itsits in — without a floor the input keeps shrinking until nothing but thepadding is left and there is nowhere to type. */--min-width: 90px;display: inline-flex;flex-direction: row;position: relative;width: 140px;min-width: var(--min-width);height: min-content;border: 1px solid var(--border-color);border-radius: var(--border-radius);outline: none;transition: all 0.1s;&:hover {--border-color: oklch(65.335% 0 0);}&:focus-within {--border-color: var(--active-color);box-shadow: 0px 0px 0px 2pxcolor-mix(in srgb, var(--active-color) 10%, transparent);}&:not(:focus-within):has(.field[data-out-of-range]) {border-color: oklch(64.245% 0.21266 25.499);}/* Only leave room for the stepper when one was composed in. */&:not(:has(.stepper)) {--stepper-width: 0px;}}.field {display: block;color: inherit;width: calc(100% - 20px - var(--stepper-width));background-color: inherit;font: inherit;padding: 6px 10px;margin: 0;border: none;outline: none;appearance: none;&[data-readonly] {cursor: not-allowed;}}.stepper {display: flex;flex-direction: column;height: 100%;position: absolute;top: 0;right: 0;width: var(--stepper-width);transition: all 0.1s linear;/* Kept out of the way until the input is being used. */opacity: 0;transform: translateX(40%);}:is(.root:hover, .root:focus-within) .stepper {opacity: 1;transform: translateX(0);}:is(.incrementStepper, .decrementStepper) {border-inline-start: 1px solid oklch(84.522% 0 0);font-size: 0.75rem;display: flex;justify-content: center;align-items: center;flex: 1 1 0%;max-height: 50%;user-select: none;cursor: pointer;&:not([data-disabled]):active {background-color: oklch(37.798% 0.03812 254.612 / 0.055);}&[data-disabled],&[data-readonly] {cursor: not-allowed;}}.incrementStepper {border-top-right-radius: var(--border-radius);}.decrementStepper {border-top: 1px solid oklch(84.522% 0 0);border-bottom-right-radius: var(--border-radius);}/** Dark mode follows whatever marks the page — `.dark` and `[data-theme=dark]`* are what this site and Storybook set.*/:global(.dark) .root,:global([data-theme='dark']) .root {--border-color: oklch(52% 0 0);}
<script lang="ts">
import { NumberInput } from '@tremolo-ui/svelte'
// Copy this file from the Styling page into your own project.
import numberInputTheme from './NumberInput.module.css'
let value = $state(64)
</script>
<NumberInput.Root class={numberInputTheme.root} bind:value min={0} max={100}>
<NumberInput.InputField class={numberInputTheme.field} aria-label="Value" />
<NumberInput.Stepper class={numberInputTheme.stepper}>
<NumberInput.IncrementStepper class={numberInputTheme.incrementStepper} />
<NumberInput.DecrementStepper class={numberInputTheme.decrementStepper} />
</NumberInput.Stepper>
</NumberInput.Root>
<script setup lang="ts">
import { ref } from 'vue'
import {
NumberInput,
NumberInputDecrementStepper,
NumberInputField,
NumberInputIncrementStepper,
NumberInputStepper,
} from '@tremolo-ui/vue'
// Copy this file from the Styling page into your own project.
import numberInputTheme from './NumberInput.module.css'
const value = ref(64)
</script>
<template>
<NumberInput
v-model="value"
:class="numberInputTheme.root"
:min="0"
:max="100"
>
<NumberInputField :class="numberInputTheme.field" aria-label="Value" />
<NumberInputStepper :class="numberInputTheme.stepper">
<NumberInputIncrementStepper :class="numberInputTheme.incrementStepper" />
<NumberInputDecrementStepper :class="numberInputTheme.decrementStepper" />
</NumberInputStepper>
</NumberInput>
</template>
インポート
- React
- Svelte
- Vue
import { NumberInput } from '@tremolo-ui/react'
import { NumberInput } from '@tremolo-ui/svelte'
import {
NumberInput,
NumberInputDecrementStepper,
NumberInputField,
NumberInputIncrementStepper,
NumberInputStepper,
} from '@tremolo-ui/vue'
API リファレンス
各パートには、そのパート独自の props を載せています。これに加えて、描画する要素の属性(className や style など)も受け取ります。
Svelte / Vue でも props は同じで、書き方だけがそれぞれの流儀になります(className ではなく class、bind:value や v-model、中身は snippet やスロットなど)。Svelte と Vue を参照してください。
状態は data 属性として要素に乗っているので、セレクタ 1 つで届きます(スタイリングを参照)。属性はその状態が on の間だけ付きます。
Root
値と、その書式・読み取り・ステップの仕方を持ちます。各パートの振る舞いはここで決めます。
| Prop | 型 | 既定値 |
|---|---|---|
value* | number | — |
min | number | — |
max | number | — |
step | number | 1 |
scale | Scale | linearScale |
format | (value: number) => string | — |
parse | (text: string) => number | — |
clampValue | boolean | true |
wheel | ModifierValue<InputEventOption> | null | ['raw', 1] |
keyboard | ModifierValue<InputEventOption> | null | { default: ['raw', 1], shift: ['raw', 0.1] } |
drag | number | null | 1 |
dragSensitivity | ModifierValue<number> | { default: 1, shift: 0.1 } |
pointerLock | boolean | false |
dragCursor | Cursor | 'ns-resize' |
selectOnFocus | "number" | "none" | "all" | 'none' |
unformatOnFocus | boolean | false |
keepCaretOnStep | boolean | false |
blurOnEnter | boolean | true |
disabled | boolean | — |
readOnly | boolean | — |
onChange | (value: number) => void | — |
onChangeStart | (value: number, source: ChangeSource) => void | — |
onChangeEnd | (value: number, source: ChangeSource) => void | — |
changeEndDelay | number | 500 |
children* | ReactNode | — |
actionsRef | Ref<NumberInputMethods> | — |
すべての説明: NumberInputProps
| Data 属性 | 値 |
|---|---|
[data-disabled] | disabled のときに付きます |
[data-readonly] | readonly のときに付きます |
InputField
テキストの入力欄で、値を打ち込める唯一の場所。これ自体がコントロールで、フォーカスを受け取り ARIA を持ちます。
独自の props は無く、<input> の属性を受け取ります。フォーカス・Enter・矢印キーでの振る舞いは NumberInput.Root で決めます(selectOnFocus、unformatOnFocus、keepCaretOnStep、blurOnEnter)。
| Data 属性 | 値 |
|---|---|
[data-disabled] | disabled のときに付きます |
[data-readonly] | readonly のときに付きます |
[data-out-of-range] | 値が min / max の範囲外にある間に付きます |
Stepper
ステッパーを入れる領域で、それ自体がドラッグのハンドルでもあります。上下にドラッグすると、drag ピクセルごとに値が 1 step 動きます。NumberInput.IncrementStepper と NumberInput.DecrementStepper はこの中に置きます。
独自の props は無く、<div> の属性を受け取ります。
IncrementStepper / DecrementStepper
押している間繰り返しながら、値を 1 step 上げる / 下げるボタン。
独自の props は無く、<div> の属性を受け取ります。子を渡すと既定の矢印を置き換えます。
| カスタムプロパティ | 読むもの | 決めるもの |
|---|---|---|
--stepper-icon-size | コンポーネント | 既定の矢印の大きさ。自分の CSS で指定します(children を渡した場合の大きさは自分で決めます)。 |
| Data 属性 | 値 |
|---|---|
[data-disabled] | disabled のとき、または値が既に max(increment)/ min(decrement)にあるときに付きます |
[data-readonly] | readonly のときに付きます |
例
単位
入力欄は format(value) を表示し、parse で読み戻します。2 つは噛み合っている必要があるので、unitFormat は両方を一度に作り、組にして展開します。
// Hertz takes SI prefixes, so 1234 is shown as 1.23kHz. const hz = unitFormat('Hz', { digits: 2 }) // Decibels do not: `d` is already deci, and -6dB is not -0.6B. const dB = unitFormat('dB', { prefixes: false, digits: 1 }) // This one is stored in milliseconds, so 1500 is shown as 1.5s. const ms = unitFormat('s', { base: 'm', digits: 2 }) function App() { const [frequency, setFrequency] = useState(1234) const [gain, setGain] = useState(-6.25) const [release, setRelease] = useState(1500) return ( <div style={{ display: 'flex', gap: '1rem', justifyContent: 'center' }}> <NumberInput.Root className={numberInputTheme.root} {...hz} value={frequency} min={20} max={22000} onChange={setFrequency} > <NumberInput.InputField className={numberInputTheme.field} aria-label="Frequency" /> </NumberInput.Root> <NumberInput.Root className={numberInputTheme.root} {...dB} value={gain} min={-60} max={6} step={0.1} onChange={setGain} > <NumberInput.InputField className={numberInputTheme.field} aria-label="Gain" /> </NumberInput.Root> <NumberInput.Root className={numberInputTheme.root} {...ms} value={release} min={1} max={10000} onChange={setRelease} > <NumberInput.InputField className={numberInputTheme.field} aria-label="Release time" /> </NumberInput.Root> </div> ) }
/** The demo theme for `NumberInput`, as a CSS Module: one class per part, applied by* the caller. Copy this file into your project and edit it.*/.root {--border-radius: 4px;--stepper-width: 20px;--stepper-icon-size: 12px;--active-color: oklch(59.5% 0.17548 266.373);--border-color: oklch(81% 0 0);/* Room for the padding, the stepper, and a few digits. A flex row that runsshort shrinks its items, and the field inside is sized from the box itsits in — without a floor the input keeps shrinking until nothing but thepadding is left and there is nowhere to type. */--min-width: 90px;display: inline-flex;flex-direction: row;position: relative;width: 140px;min-width: var(--min-width);height: min-content;border: 1px solid var(--border-color);border-radius: var(--border-radius);outline: none;transition: all 0.1s;&:hover {--border-color: oklch(65.335% 0 0);}&:focus-within {--border-color: var(--active-color);box-shadow: 0px 0px 0px 2pxcolor-mix(in srgb, var(--active-color) 10%, transparent);}&:not(:focus-within):has(.field[data-out-of-range]) {border-color: oklch(64.245% 0.21266 25.499);}/* Only leave room for the stepper when one was composed in. */&:not(:has(.stepper)) {--stepper-width: 0px;}}.field {display: block;color: inherit;width: calc(100% - 20px - var(--stepper-width));background-color: inherit;font: inherit;padding: 6px 10px;margin: 0;border: none;outline: none;appearance: none;&[data-readonly] {cursor: not-allowed;}}.stepper {display: flex;flex-direction: column;height: 100%;position: absolute;top: 0;right: 0;width: var(--stepper-width);transition: all 0.1s linear;/* Kept out of the way until the input is being used. */opacity: 0;transform: translateX(40%);}:is(.root:hover, .root:focus-within) .stepper {opacity: 1;transform: translateX(0);}:is(.incrementStepper, .decrementStepper) {border-inline-start: 1px solid oklch(84.522% 0 0);font-size: 0.75rem;display: flex;justify-content: center;align-items: center;flex: 1 1 0%;max-height: 50%;user-select: none;cursor: pointer;&:not([data-disabled]):active {background-color: oklch(37.798% 0.03812 254.612 / 0.055);}&[data-disabled],&[data-readonly] {cursor: not-allowed;}}.incrementStepper {border-top-right-radius: var(--border-radius);}.decrementStepper {border-top: 1px solid oklch(84.522% 0 0);border-bottom-right-radius: var(--border-radius);}/** Dark mode follows whatever marks the page — `.dark` and `[data-theme=dark]`* are what this site and Storybook set.*/:global(.dark) .root,:global([data-theme='dark']) .root {--border-color: oklch(52% 0 0);}
unitFormat は値の大きさから SI 接頭辞を選びます。1234 は 1.23kHz、0.0005 は 500µHz と表示され、0 は基本単位のままです。読み戻すときは µ・μ・u をどれもマイクロとして読みます。キーボードで打てるのは最後の 1 つだけだからです。
それが望む形でない場合のために、オプションが 2 つあります。
prefixes: falseは数字に手を付けず、単位記号を書いたとおりに付け足します。dB、%、セント、半音のような SI の量でないものに使います。dB に接頭辞を付けるのは珍しいどころか誤りです。dはデシなので、-6dBが -0.6B と読まれてしまいます。baseは、持っている値が既にどの接頭辞の単位かを指定します。ミリ秒で値を持つコントロールならunitFormat('s', { base: 'm' })で、1500 は1.5sと表示され、parseは 1500 を返します。
表示で見せられるもの
丸めた表示も細かい step も、それぞれ単独なら問題無く、互いに照らし合わせることもしません。format はいくら丸めても構いません。値は自分の精度を保ち、unformatOnFocus を使えばクリックしたときに素の数字が入力欄に出ます。
避けたいのは、キーを押しても変化がまったく見えないほど粗い表示です。秒の値を小数 2 桁で表示すると 1 ミリ秒のステップは見えず、入力欄は壊れているように見えます。開発ビルドでは、それが起きたときに警告を出します:
[tremolo-ui] NumberInput: `keyboard` moves the value, but `format` shows the
same text before and after, everywhere in the range. The display is too coarse
for it to be seen.
keyboard や wheel の量が step より細かく、押すたびに元の位置へ丸め戻されてしまう場合も、同じ警告が出ます。本番ビルドには、検査もメッセージも含まれません。
format と parse は普通の props なので、この形に合わない単位は自分で書けます。parse は読めないテキストに対して NaN を返してください。そうすると入力欄は、誰も打っていない数字を確定する代わりに、元の値を保ちます。
書式付きの値を編集する
format はレンダーのたびに走るので、1.23kHz と表示している入力欄は、クリックしても 1.23kHz のままです。これを編集するには、全体を打ち直すか、単位の文字列の中にキャレットを置くことになります。
NumberInput.Root の unformatOnFocus は、入力欄にフォーカスがある間は書式を外し、代わりに素の値を表示します。
<NumberInput.Root {...unitFormat('Hz', { digits: 2 })} value={1230} unformatOnFocus …>
<NumberInput.InputField />
</NumberInput.Root>
not focused 1.23kHz
focused 1230
**表示されるのは値で、書式付きのテキストの中にある数字ではありません。**書式が桁を変える場合、この 2 つは食い違います。1.23 を編集用に出すと 1.23 と読み戻され、1000 倍ずれてしまいます。
丸めた表示が値に漏れなくなるのも、同じ理由です。digits: 0 の入力欄は 1.6 という値を 2Hz と表示しますが、フォーカスすると 1.6 を出すので、編集して確定しても値が 2 に丸められることはありません。
selectOnFocus も効き、選択範囲は画面に出たテキストに対して測ります。書式を外していれば、テキスト全体が数字なので、'number' と 'all' は同じことになります。
どちらの props も既定では off なので、指定しない限り何も変わりません。
桁をステップする
矢印キーは value 全体を差し替え、値を差し替えられた制御された <input> はキャレットを末尾に落とします。そのため押し続けると、2 回目以降は必ず最後の桁に効きます。
keepCaretOnStep はキャレットを元に戻します。
<NumberInput.Root value={value} keepCaretOnStep …>
<NumberInput.InputField />
</NumberInput.Root>
位置はどちらかの端からではなく小数点から測るので、数字の桁数が変わってもずれません。9 と .9 の間にあったキャレットは、10 と .0 の間に来ます。先頭から測っていたら、桁が 1 つ増えたときに違う列へずれていたところです。
戻すのはキャレットだけです。どの桁にあってもステップの大きさは変わりません。それを決めるのは keyboard です。既定では off です。