```
## 端では押せません [#端では押せません]
**移動先が無いのに押せるのは筋が通りません。** 1枚しか無ければ、前も次も押せません。
## 折り返しをいつ使うか [#折り返しをいつ使うか]
折り返す形(`options={{ loop: true }}`)は既定ではありません。
| | |
| --------- | ------------------------------------------ |
| **入れてよい** | 写真・広告・お客様の声のように、**順番に意味が無い**もの。どこから見ても構わない |
| **入れない** | 手順・記事の並び・検索結果のように、**順番に意味がある**もの |
折り返すと「最後まで来た」が分からなくなります。
**順番に意味があるものでは、終わりに着いたことが読み取れなくなります。**
## 現在位置はスクリーンリーダーにも伝わります [#現在位置はスクリーンリーダーにも伝わります]
位置表示のボタンには `aria-current` が付きます。
**色の違いだけで示すと、色が見えない利用者や読み上げには届きません。**
## ボタンと位置表示は、スクロールする枠の外に置いてください [#ボタンと位置表示はスクロールする枠の外に置いてください]
`CarouselSlides` がスクロールする枠を描きます。**その中に置くと一緒にスクロールしてしまい**、
2枚目へ進んだ瞬間に画面の外へ出ます。見た目には「ボタンが消えた」としか映りません。
## 自動で動かすときは、停止ボタンが必須です [#自動で動かすときは停止ボタンが必須です]
`autoplay` を使うときは `CarouselPlayPause` を一緒に置いてください。
**置かないとエラーになります。**
```tsx
…
```
## 4つのボタンは、それぞれ別の役目です [#4つのボタンはそれぞれ別の役目です]
**停止ボタンは、前後のボタンの代わりではありません。**
| | 役目 |
| ------ | -------------- |
| 前へ・次へ | **場所を移す** |
| 止める・再生 | **勝手に動くのを制する** |
| 印 | **いまどこか**を示す |
**同じ場所に置くと片方が消えます。** 停止ボタンを「前へ」の位置に置くと、前へ移動する手段が無くなります。
例からどちらかが欠けたらテストが失敗するようにしてあります。
自動で動くものには**止める手段が無ければなりません**(WCAG 2.2.2)。
文書に書くだけでは守られないので、**組み立ての側で成立しない形**にしています。
**黙って動き続けるほうが害が大きい**——読んでいる最中に勝手に送られ、止める方法がありません。
間隔の既定は 4000ms です。**選んだ値で、段から導いてはいません。**`autoplay={{ delay: 8000 }}` のように渡せます。
触ったとき・ポインタが乗ったとき・フォーカスが入ったときは止まります。
**読んでいる最中に送られるのは邪魔です。**
**動きを減らす設定のときは、止まった状態で始まります。**
黙って動かしませんが、動かす手段は残します。
## 位置の見た目を自分で決めたいときは `useCarouselPosition` [#位置の見た目を自分で決めたいときは-usecarouselposition]
サムネイルの列や「3 / 7」のような表示は、このフックで組めます。渡されるのは3つだけです。
| | |
| ------------- | ---------------- |
| `selected` | いま見えている枚目(0 始まり) |
| `count` | 何枚あるか |
| `goTo(index)` | その枚へ飛ぶ |
```tsx
function Thumbnails({ photos }: { photos: { src: string; alt: string }[] }) {
const { selected, count, goTo } = useCarouselPosition();
// 1枚しかないなら出しません。押しても何も起きないものを置かないためです
if (count <= 1) return null;
return (
{photos.map((photo, index) => (
-
))}
);
}
```
**いまどこを見ているかは、見た目だけでなく属性でも伝えてください**(`aria-current`)。枠線の色だけで表すと、見えない人に届きません。
**送りの仕組みそのものは渡していません。** 渡すと、その仕組みの API がそのまま約束になり、入れ替えたときにあなたのコードが壊れます。前後のボタンと位置の印で足りるなら、`CarouselPrevious` / `CarouselNext` / `CarouselMarkers` を使ってください。
## 動きを減らす設定ではアニメーションなしで切り替わります [#動きを減らす設定ではアニメーションなしで切り替わります]
スクロールのアニメーションは止まり、すぐに切り替わります。**利用側で何かする必要はありません。**
### 例: default
```tsx
import { Card } from '../../card/card.tsx';
import {
Carousel,
CarouselMarkers,
CarouselNext,
CarouselPrevious,
CarouselSlide,
CarouselSlides,
} from '../carousel.tsx';
/**
* 通常。**1枚ずつスクロールする。**
*
* 前後のボタンは端で押せなくなる。印は押すとそこへ飛ぶ。
* **掴んで引く操作も効く**——スクロールの仕組みは Embla が持っている。
*/
export default function Default() {
return (
{['一', '二', '三', '四'].map((n) => (
{n}枚目の中身
))}
);
}
```
### 例: empty
```tsx
import { Carousel, CarouselMarkers, CarouselSlides } from '../carousel.tsx';
/**
* 空。**1枚も無いとき。**
*
* 印は出ない——**数はスライドから来る**ので、無ければ0個になる。
* 枠だけが残り、潰れない。
*/
export default function Empty() {
return (
);
}
```
### 例: edge
```tsx
import { Card } from '../../card/card.tsx';
import {
Carousel,
CarouselMarkers,
CarouselNext,
CarouselPlayPause,
CarouselPrevious,
CarouselSlide,
CarouselSlides,
} from '../carousel.tsx';
/**
* エッジケース。**1枚だけのときと、端で折り返すとき。**
*
* 1枚しか無ければ前も次も押せない。**移動先が無いのに押せるのは筋が通らない。**
*
* 折り返す形は既定ではない。**順番に意味が無いときだけ入れる**——
* 写真や広告のように、どこから見ても構わないもの。
* **手順や記事の並びには入れない。** 折り返すと「最後まで来た」が分からなくなる。
*
* 自動でスクロールさせるときは**停止ボタンが必須**である。置かないと落ちる——
* 自動で動くものには止める手段が要る。
* 動きを減らす設定のときは**止まった状態で始まる。**
*
* **停止ボタンは、前後のボタンの代わりではない。** 4つとも別の役目を持つので、
* 同じ場所に置くと片方が消える。
*/
export default function Edge() {
return (
これだけ
{['一', '二', '三'].map((n) => (
{n}枚目
))}
{/*
**停止ボタンは、前後のボタンの代わりではない。** 4つとも別の役目を持つ。
前へ・次へは**場所を移す**もの、止める・再生は**勝手に動くのを制する**もの、
印は**いまどこか**を示すもの。
同じ場所に置くと、片方が消える。
*/}
{['一', '二', '三'].map((n) => (
{n}枚目
))}
);
}
```
# Checkbox (/docs/components/checkbox)
## 素の input に乗っています [#素の-input-に乗っています]
読み上げ・キーボード・フォームへの載り方は、**どれもブラウザが持っています。**
差し替えているのは見た目だけです。
入った印は兄弟の要素に置いて重ねています(`input` の中に要素は置けません)。
出し入れは CSS の `peer-checked:` なので、**このコンポーネントは状態を持ちません。**
持たせると、値の出どころが `input` とこのコンポーネントの2つになります。
いまは `input` だけが値を持つので、素のフォームでも `react-hook-form` の
`register()` でも、そのまま載ります。
## ラベルは Field が付けます [#ラベルは-field-が付けます]
**ラベルを自分では持っていません。** 持つと結びつけの経路が2つになり、
片方だけ直したときにずれます。
ラベルが右に来る配置は Field が持っています。
```tsx
```
## チェックの印は塗りを宣言しています [#チェックの印は塗りを宣言しています]
`bg-accent` のような**塗るだけの道はありません**。塗りは宣言するもので、
宣言すると背景と前景が対で決まります——中の印は文字色を継ぐので、読める側になります。
押せないときだけ段が落ちます。宣言は CSS では差し替えられませんが、
`disabled` は**描くときに分かる**ので、そこで分けています。
### 例: default
```tsx
import { Field } from '../../field/field.tsx';
import { Checkbox } from '../checkbox.tsx';
/**
* 通常。**ラベルは Field が付ける。**
*
* `layout="inline"` でラベルが右に来る。上に置くと、
* ラベルがどの選択肢のものか目で辿れない。
*/
export default function Default() {
return (
);
}
```
### 例: empty
```tsx
import { Checkbox } from '../checkbox.tsx';
/**
* 空。**ラベルも説明も無い。**
*
* 表の行を選ぶような、**ラベルを置く場所が無い**場面がある。
* そのときは `aria-label` で名前を渡す——名前が無いと読み上げが「チェックボックス」としか言わない。
*/
export default function Empty() {
return ;
}
```
### 例: edge
```tsx
import { Field } from '../../field/field.tsx';
import { Checkbox } from '../checkbox.tsx';
/**
* エッジケース。**押せないときと、誤りのとき。**
*
* 押せないときは塗りの段が落ちる。`data-sg-fill` は宣言なので CSS では
* 差し替えられないが、**`disabled` は描くときに分かる。**
*
* 誤りのときは線が変わる。**線だけで伝えない**——文言も一緒に出す。
*/
export default function Edge() {
return (
);
}
```
# Field (/docs/components/field)
## ラベルと入力の関連付けは Field が作ります [#ラベルと入力の関連付けは-field-が作ります]
ラベルと入力、説明と入力、誤りと入力。**どれも忘れると読み上げだけが黙ります。**
見た目は正常なので、書き忘れても気づけません。
`Field` に `id` を1つ渡せば、ラベル・説明・誤りの関連付けは全部こちらで作ります。
```tsx
```
**利用側が渡すのは `id` を1つだけ**です。説明と誤りの `id` はそこから導きます。
## `id` は必須です [#id-は必須です]
自動で作る道は取っていません。安定した名前を作るには `useId` が要り、
**このコンポーネントがクライアント側になる**ためです。
必須にしたことで、**書き忘れが型で止まります。**
## バリデーションは持ちません [#バリデーションは持ちません]
**規則を走らせる仕組みはありません。** 誤りの文言を受け取るだけです。
受け取るのは標準の props だけなので、**どのライブラリとも組めます**——
[react-hook-form](https://react-hook-form.com/) でも [TanStack Form](https://tanstack.com/form) でも
[Conform](https://conform.guide/) でも、素の `FormData` でも同じです。
```tsx
{/* react-hook-form の例 */}
```
### 実際に動かせます [#実際に動かせます]
下は **react-hook-form で検証が走る本物のフォーム**です。
打っている最中に検証が走ります——形が整うと線が緑に変わり、印が出ます。
**`Field` は `react-hook-form` に依存していません。** このデモが使っているだけで、
受け取るのは標準の props だけです。`register()` が返すものをそのまま渡せます。
### 入力側で同じ属性を書くとエラーになります [#入力側で同じ属性を書くとエラーになります]
`id` / `aria-describedby` / `aria-invalid` は **Field が渡します。**
入力側で書くと**Field が渡した値が上書きされ、関連付けだけが切れます**——
見た目は正常なままなので気づけません。だから**エラーにしています。**
`register()` が返す `name` / `onChange` / `onBlur` / `ref` は重なりません。
## 入力が正しいことも示せます [#入力が正しいことも示せます]
```tsx
```
境界が変わり、**入力の右端に印**が出ます。印は読み上げから隠れています——
出しても「チェック」としか言わないので、**状態は境界が伝えます。**
誤りと同時には使えません。**両方は成り立たない**ので、誤りが勝ちます。
## 空の段落は置きません [#空の段落は置きません]
説明も誤りも無いときは、**何も描きません。** 置くと、
何も書かれていない行が読み上げに出ます。
## 必須であることを記号だけで伝えません [#必須であることを記号だけで伝えません]
`*` は読み上げに届かないので、**文字も一緒に置いています。**
### 例: default
```tsx
import { Input } from '../../input/input.tsx';
import { Field } from '../field.tsx';
/**
* 通常。**結びつけを利用側に書かせない。**
*
* ラベルと入力、説明と入力。どれも忘れると読み上げだけが黙る——
* 見た目は正常なので、書き忘れても気づけない。
*
* 利用側が渡すのは `id` を1つだけである。
*/
export default function Default() {
return (
);
}
```
### 例: empty
```tsx
import { Input } from '../../input/input.tsx';
import { Field } from '../field.tsx';
/**
* 空。**説明も誤りも無いとき。**
*
* ラベルと入力だけが残る。**空の段落を置かない**——
* 置くと、何も書かれていない行が読み上げに出る。
*/
export default function Empty() {
return (
);
}
```
### 例: edge
```tsx
import { Input } from '../../input/input.tsx';
import { Textarea } from '../../textarea/textarea.tsx';
import { Field } from '../field.tsx';
/**
* エッジケース。**誤りと説明が両方あるとき、必須のとき。**
*
* 両方あるときは**両方が読み上げに届く**——片方だけにすると、
* もう片方が黙る。
*
* 必須の印は記号だけでは読み上げに届かないので、**文字も一緒に置く。**
*
* 満たしていることも表せる。**誤りとは別の仕組みで受け取る**——
* 誤りには `aria-invalid` という標準の属性があるが、
* 満たしていることを表す属性は無い。
*
* 規則は持たない。**誤りの文言を受け取るだけ**である——
* 走らせるのは利用側で、どのライブラリとも組める。
*/
export default function Edge() {
return (
);
}
```
# Form (/docs/components/form)
## バリデーションは持ちません [#バリデーションは持ちません]
規則を走らせる仕組みはありません。**走らせるのは利用側**です。
ここが持つのは、**利用側が書き忘れるもの**だけです。
## 送信に失敗したら、最初の誤りへフォーカスが移ります [#送信に失敗したら最初の誤りへフォーカスが移ります]
長いフォームでは、**誤りが画面の外にあると何が起きたのか分かりません。**
送信ボタンを押しても、何も起きていないように見えます。
誤りを名乗っている欄(`aria-invalid`)を探すので、
**どのライブラリで検証したかに依りません。**
移す先は**全体の誤り → 最初の誤りの欄**の順です。全体の誤りは
「なぜ送れなかったか」を説明していて、フォームの上端にあります。
そこへ移せば、下へ読み進める形になります。
### 検証が非同期なら、Promise を返してください [#検証が非同期ならpromise-を返してください]
いつ誤りが付くかは、送信処理が終わるまで分かりません。
**Promise を返せば、その解決を待ちます**——`async` で書けばそうなります。
```tsx
);
}
```
### 例: empty
```tsx
'use client';
/*
**例そのものをクライアント側にする。** Form はクライアント側のコンポーネントなので、
サーバ側から送信処理(関数)を渡せない。
*/
import { Button } from '../../button/button.tsx';
import { Form, FormActions } from '../form.tsx';
/**
* 空。**欄が1つも無い。**
*
* 押すだけのフォーム(確認して送るだけ、など)がある。
* **欄が無くても崩れないこと**を見せておく。
*/
export default function Empty() {
return (
);
}
```
### 例: edge
```tsx
'use client';
/*
**例そのものをクライアント側にする。** Form はクライアント側のコンポーネントなので、
サーバ側から送信処理(関数)を渡せない。
*/
import { Button } from '../../button/button.tsx';
import { Field } from '../../field/field.tsx';
import { Input } from '../../input/input.tsx';
import { Form, FormActions } from '../form.tsx';
/**
* エッジケース。**フォーム全体の誤りと、欄の誤りが同時にある。**
*
* 全体の誤りは**指す先が無い**——どの欄が悪いとも言えないので、
* `aria-describedby` では結べない。その場で読み上げに届く形で出す。
*/
export default function Edge() {
return (
);
}
```
# Icon (/docs/components/icon)
## アイコンは lucide のものです [#アイコンは-lucide-のものです]
[**lucide**](https://lucide.dev/) のアイコンを使います。実行時の依存を持たず、必要なものだけがバンドルに含まれます。
このシステムが持つのは**サイズ・読み上げの既定・`data-sg-component` の付与**だけです。
アイコンそのものは lucide のものです。
## `lucide-react` は一緒にインストールされます [#lucide-react-は一緒にインストールされます]
Icon を追加すると `lucide-react` も一緒にインストールされます。**自分で入れる必要はありません。**
ただし**ここに無いアイコンを足すときは、利用側が `lucide-react` から import します**——
アイコンそのものは lucide のものだからです。
## ここに無いアイコンは、同じ形で足せます [#ここに無いアイコンは同じ形で足せます]
包む形を公開しています。**全部を持たない代わりに、足す道を配ります。**
```tsx
import { Search } from 'lucide-react';
import { defineIcon } from '@sashigane/ui';
export const IconSearch = defineIcon(Search);
```
props は lucide のものをそのまま引き継ぎます。
**`data-sg-component` の値はアイコンの名前から導きます。** `Search` なら `icon-search`、
`ChevronDown` なら `icon-chevron-down` です。**手で書きません**——
書くと2箇所に同じことが並び、片方だけ直したときに静かにずれます。
## 既定は読み上げから隠れます [#既定は読み上げから隠れます]
アイコンはたいてい**文字の隣にあり、同じことを言っています。**
隠さないと二重に読まれます。
名前を渡したときだけ隠れません。
```tsx
{/* 飾り。読み上げに出ません */}
{/* アイコンだけで意味を伝える場所 */}
```
## 大きさは段で選びます [#大きさは段で選びます]
| 段 | 使うところ |
| -------- | ------------------------ |
| `sm` | ラベルや表の行の中 |
| `md`(既定) | 文字やボタンと並べるとき。**行の高さと同じ** |
**`className` で大きさは上書きできません。** 同じ次元を上書きしたとき、
どちらが効くかをこのシステムは保証していないためです——
実際 `size-4` を渡しても既定に負けます。**段にすれば、負ける形が生まれません。**
## `size` は通していません [#size-は通していません]
lucide は `width` / `height` の**属性**で書きますが、こちらはクラスで書きます。
**CSS は属性に勝つ**ので、`size` を渡しても**何も起きないまま消えます。**
### 例: default
```tsx
import { IconPlus, IconX } from '../icon.tsx';
/**
* 通常。**アイコンは lucide から来る。**
*
* サイズは行の高さに合わせてある。文字やボタンと横に並べたときに揃う。
* 色は継承する——置いた場所の前景に従う。
*/
export default function Default() {
return (
文字と並べる
);
}
```
### 例: empty
```tsx
import { IconX } from '../icon.tsx';
/**
* 空。**中身を持たない。**
*
* アイコンそのものが中身なので、空の状態はこれ1つしかない。
* 既定では読み上げから隠れている——**文字の隣で同じことを言っている**ためである。
*/
export default function Empty() {
return ;
}
```
### 例: edge
```tsx
import { Button } from '../../button/button.tsx';
import { IconPlus, IconX } from '../icon.tsx';
/**
* エッジケース。**名前を与えたときと、塗りの上に置いたとき。**
*
* 既定は読み上げから隠れる。名前を渡すと隠れなくなる——
* **アイコンだけで意味を伝える場所**では、名前が要る。
*
* 色は継承するので、塗りの上でも枠の中でも書き直さずに読める。
*/
export default function Edge() {
return (
);
}
```
# Input (/docs/components/input)
## 凹んだ面を宣言します [#凹んだ面を宣言します]
入力欄は**地**であって塗りではありません。面の仕組みに乗るので、
背景と文字が対で決まります。**塗るだけの道はありません。**
## 誤りは自分で決めません [#誤りは自分で決めません]
`aria-invalid` が付くと境界が変わります。**付けるのは Field です**——
ここで決めると、Field を使わない書き方だけが誤りを名乗れることになります。
境界の色は**文字の役割**(`border-danger`)です。mark ではありません——
深い面の上では mark が要件ぎりぎりまで明るい側へ寄るので、
**赤のように、明るい側で彩度が落ちる色は白っぽく見えます。**
| ダークモード・凹んだ面での彩度 | |
| --------------- | ---------------- |
| `danger-mark` | C\* = 21.4(白っぽい) |
| `danger` | **C\* = 40.8** |
**状態の境界は太くします。** 1px では色の面積が足りず、違いが読み取りにくいためです。
**境界だけで伝えません。** Field が文言も一緒に出します。
## 満たしていることも表せます [#満たしていることも表せます]
`valid` を渡すと、境界が変わり、印が出ます。
誤りとは**別の仕組みで受け取ります**——誤りには `aria-invalid` という
標準の属性がありますが、**満たしていることを表す属性はありません。**
そこだけ形が違うのはそのためです。
## 押せないときは文字が淡くなります [#押せないときは文字が淡くなります]
**不透明度では表しません。** 薄めると前景と背景が同時に下地へ寄り、
読みやすさの保証が効かなくなります。
### 例: default
```tsx
import { Field } from '../../field/field.tsx';
import { Input } from '../input.tsx';
/**
* 通常。**凹んだ面を宣言している。**
*
* 入力欄は地であって塗りではない。背景と文字が対で決まるので、塗るだけの道が無い。
*
* ラベルとの結びつけは Field が作る。単体でも置けるが、
* **そのときは結びつけを利用側が書くことになる。**
*/
export default function Default() {
return (
);
}
```
### 例: empty
```tsx
import { Input } from '../input.tsx';
/**
* 空。**中身もラベルも無いとき。**
*
* 単体で置くとラベルとの結びつけが無い。**読み上げは「編集欄」としか言わない**ので、
* 実際には Field と一緒に使う。ここでは枠そのものの見た目を見る。
*/
export default function Empty() {
return (
);
}
```
### 例: edge
```tsx
import { Field } from '../../field/field.tsx';
import { Input } from '../input.tsx';
/**
* エッジケース。**誤りのときと、押せないとき。**
*
* 誤りの境界は mark の段を使う。**文字ではないものに要る 3:1 を満たす段**で、
* 役割としても合っている。
*
* 押せないときは文字が淡くなる。**不透明度では表さない。**
*/
export default function Edge() {
return (
);
}
```
# List (/docs/components/list)
## 行頭の記号は既定で出しません [#行頭の記号は既定で出しません]
素の `ul` の点や数字は初期化で消えます。**戻すのは選んだときだけ**にしています——
並べたものが文章とは限らないためです(ラベルの一覧、設定の行)。
```tsx
…
{/* 記号なし */}
…
{/* 点 */}
…
{/* 数字 */}
```
## 順序があるリストには `ordered` を付けてください [#順序があるリストには-ordered-を付けてください]
`ordered` を付けると `ol` になります。**見た目ではなくマークアップが変わる**ので、
読み上げが「1番目の項目」のように順序を伝えられるようになります。
**数字が見えているだけでは足りません。** `marker="number"` は見た目だけを変え、
`ordered` はマークアップを変えます。順序のあるリストには**両方**付けてください。
## 区切り線は各項目の上辺に引かれます [#区切り線は各項目の上辺に引かれます]
**先頭の項目だけ線が付きません。** リストの上端に線が残ると、外側に枠があるように
見えてしまうためです。
線を付けると**項目どうしの余白は詰まります**——線が区切りの役目を担うので、
余白と二重にしません。
```tsx
設定の行
線で区切られています
```
### 枠から項目へは自動で降りません [#枠から項目へは自動で降りません]
`separated` は枠と項目の両方に書きます。**`li` は枠の直下とは限らず**、
間に何かを挟んだときに静かにずれるためです。
## 項目の中は自由です [#項目の中は自由です]
`ListItem` は `li` を作るだけです。リンクにしたいときは**中にリンクを置いてください**——
`li` そのものを差し替えると、並びの意味が壊れます。
### 例: default
```tsx
import { List, ListItem } from '../list.tsx';
/**
* 通常。**行頭の記号は既定で出さない。**
*
* 素の点や数字は初期化で消えるので、戻すのは選んだときだけである——
* 並べたものが文章とは限らない(ラベルの一覧、設定の行)。
*
* 順序のある並びは `ordered` を付ける。**見た目ではなく意味**で、
* 読み上げが順序を伝えられるようになる。
*/
export default function Default() {
return (
印の無い並び
枠は縦に並べて間を空けるだけ
点の付いた並び
文章の中で使う
順序のある並び
読み上げも順序を伝えます
);
}
```
### 例: empty
```tsx
import { List, ListItem } from '../list.tsx';
/**
* 空。**項目が無いとき。**
*
* 枠だけを描いても何も見えない。**空であることは1つの項目で言う**——
* 枠ごと消すと、何を待っているのかが分からなくなる。
*/
export default function Empty() {
return (
まだ何もありません
);
}
```
### 例: edge
```tsx
import { List, ListItem } from '../list.tsx';
/**
* エッジケース。**線で区切るときと、項目の中身が長いとき。**
*
* 線は項目が持つ。**最初の項目だけ線を持たない**ので、上端に線が残らない——
* 残ると枠の枠に見える。
*
* 線を選ぶと項目の間隔は詰まる。**線が間隔を担う**ためである。
*
* `separated` は枠から項目へ自動では降りない。`li` は枠の直下とは限らず、
* **間に何かを挟んだときに静かにずれる**ためである。
*/
export default function Edge() {
return (
設定の行
線で区切られています
最初の項目だけ上に線がありません
項目の中身が長いとき。折り返しても印の位置は動かず、2行目以降は印の右側に揃います。
Supercalifragilisticexpialidocious のような長い語も枠を破りません。
);
}
```
# Modal (/docs/components/modal)
## 素の dialog に乗っています [#素の-dialog-に乗っています]
`showModal()` を呼ぶと、ブラウザが次の全部を持ってくれます。
* 最前面の層に出る(`z-index` も `overflow` も関係しない)
* **フォーカスが中に閉じ込められる**
* 後ろが操作できなくなる
* `Escape` で閉じる
* **閉じたとき、開く前にフォーカスがあった場所へ戻る**
自前で書くと、この5つを全部持ち直すことになります。
**属性だけで出すと、この層に入りません。** 入らないとフォーカスの閉じ込めも後ろの遮断も
効きませんが、**見た目は出る**ので気づけません。テストが `:modal` で測っています。
## 開いているかどうかは利用側が持ちます [#開いているかどうかは利用側が持ちます]
```tsx
const modal = useModal();
本当に消しますか
```
これは**利用側が知りたい値**です——開いている間だけ読み込む、
閉じたら次の案内を出す、など。コンポーネントの中に隠すと「開いた」ことしか分かりません。
`Escape` で閉じたときも `onClose` が呼ばれます。**返さないと、
利用側は開いていると思ったまま**になり、次に押しても開きません。
## 後ろの巻き取りは自分で止めています [#後ろの巻き取りは自分で止めています]
`showModal()` は後ろの**操作**は止めますが、**巻き取り(スクロール)は止めません。**
止めないと、中を読んでいるつもりで後ろが動きます。
## 背景を押して閉じるのは既定ではありません [#背景を押して閉じるのは既定ではありません]
`dialog` は背景の部分も自分の領域なので、**押した場所が中か外かを座標で見分ける**
ことになります。取り違えると、中を押したのに閉じます。
消える操作の前では代償が大きいので、`closeOnBackdrop` を渡したときだけにしています。
## 表示と消去にアニメーションが付きます [#表示と消去にアニメーションが付きます]
ダイアログと背景が一緒に薄れて現れ、薄れて消えます。
**閉じるときが難しいところです。** 閉じる操作は表示を即座に消すので、
何もしないと**消えてから動く**ことになり、動きが見えません。
`display` と `overlay` を `allow-discrete` で遷移させて、
**動き終わるまで最前面の層に留まる**ようにしています。
**動きを減らす設定(`prefers-reduced-motion`)では動きません。** 尊重するのは利用側の責務ではないので、
トークン層で止めています。
## 幅は決めていません [#幅は決めていません]
**このシステムには幅の段がありません。** 素の `dialog` は中身に合わせて広がり、
画面幅で止まります。幅を決めたいときは `className` か `style` で渡してください。
### 例: default
```tsx
'use client';
/*
**例そのものをクライアント側にする。** Modal はクライアント側のコンポーネントなので、
サーバ側から開閉の処理(関数)を渡せない。
*/
import { Button } from '../../button/button.tsx';
import { Modal } from '../modal.tsx';
import { useModal } from '../use-modal.ts';
/**
* 通常。**開いているかどうかは `useModal` が持つ。**
*
* フォーカスの閉じ込め・後ろを触れなくすること・`Escape` で閉じること・
* 閉じたときに元の場所へフォーカスが戻ることは、**すべてブラウザが持っている。**
*/
export default function Default() {
const modal = useModal();
return (
>
}
>
保存しないまま閉じると、書いた内容は消えます。
);
}
```
### 例: empty
```tsx
'use client';
/*
**例そのものをクライアント側にする。** Modal はクライアント側のコンポーネントなので、
サーバ側から開閉の処理(関数)を渡せない。
*/
import { Button } from '../../button/button.tsx';
import { Modal } from '../modal.tsx';
import { useModal } from '../use-modal.ts';
/**
* 空。**中身も操作も無い、見出しだけ。**
*
* 操作を置かない形はある(読むだけのダイアログ)。
* そのときも**閉じる道は残る**——右上の印と `Escape` である。
*/
export default function Empty() {
const modal = useModal();
return (
{null}
);
}
```
### 例: edge
```tsx
'use client';
/*
**例そのものをクライアント側にする。** Modal はクライアント側のコンポーネントなので、
サーバ側から開閉の処理(関数)を渡せない。
*/
import { Button } from '../../button/button.tsx';
import { Field } from '../../field/field.tsx';
import { Input } from '../../input/input.tsx';
import { Modal } from '../modal.tsx';
import { useModal } from '../use-modal.ts';
/**
* エッジケース。**中身が長く、入力があり、覆いを押すと閉じる。**
*
* 覆いで閉じるのは**指定したときだけ**である。押した場所が中か外かは
* 座標で見分けることになり、取り違えると中を押したのに閉じる。
*/
export default function Edge() {
const modal = useModal();
return (
);
}
```
# PasswordInput (/docs/components/password-input)
## `type` を入れ替えます [#type-を入れ替えます]
`type` は CSS では変えられません。**このコンポーネントは状態を持つ**ので、
サーバ側では描けません。
入れ替えるのは**同じ要素の属性だけ**です。枠ごと差し替えると、
**打っている最中にフォーカスが外れて、以降の文字が入らなくなります。**
見た目には何も出ないので、打ち続けて初めて分かります。
テストが**打ちながら切り替えて**、続けて打った文字が入ることを測っています。
## 見た目は Input から借りています [#見た目は-input-から借りています]
線も箱も入力欄と同じものを使います。**写しを作ると、片方だけ直したときにずれます。**
## ラベルが状態を伝えます [#ラベルが状態を伝えます]
ボタンのラベルが「パスワードを表示」から「パスワードを隠す」に変わります。
**目のアイコンだけでは、いまどちらなのかが読み上げに出ません。**
ラベルは差し替えられます。
```tsx
```
## 満たしている印と場所を分けます [#満たしている印と場所を分けます]
Field は満たしているとき、印を欄の末尾に重ねます。**切り替えのボタンも末尾にあります。**
そのままだと重なるので、満たしているときだけボタンを内側へ寄せています。
寄せ幅は印の位置から決まります。**当てずっぽうではありません**——
印は末尾から 12px の位置に 16px で置かれるので、末尾から 28px までを占めます。
24px では 4px 重なりました。
## 切り替えはフォームを送りません [#切り替えはフォームを送りません]
ボタンの既定の `type` は `submit` です。**そのままだと、見せようとした瞬間に送信されます。**
## 押せないときは切り替えも押せません [#押せないときは切り替えも押せません]
**中身が見えないのに切り替えだけ動くのは嘘です。**
## 値は覚えません [#値は覚えません]
このコンポーネントが覚えるのは「**見せているかどうか**」だけです。値は利用側が持ちます。
`autoComplete` は利用側が渡してください。ログインなら `current-password`、
登録なら `new-password` です。**こちらで決めると、片方が必ず間違いになります。**
### 例: default
```tsx
import { Field } from '../../field/field.tsx';
import { PasswordInput } from '../password-input.tsx';
/**
* 通常。**打った文字を確かめられる。**
*
* 末尾のボタンで見せる/隠すを切り替える。切り替えても**打っている場所は動かない**——
* 枠を差し替えず、同じ要素の `type` だけを入れ替えているためである。
*/
export default function Default() {
return (
);
}
```
### 例: empty
```tsx
import { Field } from '../../field/field.tsx';
import { PasswordInput } from '../password-input.tsx';
/**
* 空。**何も入っていないときも、切り替えのボタンは出したままにする。**
*
* 入ってから現れる形にすると、**欄の幅が打ち始めた瞬間に変わる。**
*/
export default function Empty() {
return (
);
}
```
### 例: edge
```tsx
import { Field } from '../../field/field.tsx';
import { PasswordInput } from '../password-input.tsx';
/**
* エッジケース。**誤り・満たしている・押せない。**
*
* 満たしているときは印が末尾に重なる。**切り替えのボタンと場所がぶつかる**ので、
* ボタンを内側へ寄せている。
*
* 押せないときはボタンも押せない。**中身が見えないのに切り替えだけ動くのは嘘である。**
*/
export default function Edge() {
return (
);
}
```
# Radio (/docs/components/radio)
## グループのラベルが要ります [#グループのラベルが要ります]
1つずつのラベル(「ふつう」「大きい」)だけでは、
**何についての選択なのかが読み上げに出ません。**
グループのラベルは `fieldset` と `legend` でしか作れません。`div` に `aria-label` を足す形も
動きますが、**`legend` は選択肢を読むたびにグループのラベルを添えます。**
```tsx
```
## name は利用側が渡します [#name-は利用側が渡します]
グループから配っていません。配るにはコンテキストが要り、
**このコンポーネントがクライアント側になります。** いまはどのコンポーネントもサーバ側で描けます。
`name` が揃っていないと、**全部が独立に選べる状態になります。**
これはテストが測っています。
## 誤りはグループに付きます [#誤りはグループに付きます]
選ばれていないのは選択肢1つの問題ではないので、1つずつに赤い線を引いても
**どれを直せばよいか伝わりません。**
### 例: default
```tsx
import { Field } from '../../field/field.tsx';
import { Radio } from '../radio.tsx';
import { RadioGroup } from '../radio-group.tsx';
/**
* 通常。**グループのラベルは RadioGroup が付ける。**
*
* 1つずつのラベルだけでは、**何についての選択なのかが読み上げに出ない。**
* `name` は利用側が渡す——グループから配るにはコンテキストが要り、
* このコンポーネントがクライアント側になる。
*/
export default function Default() {
return (
);
}
```
### 例: empty
```tsx
import { Radio } from '../radio.tsx';
/**
* 空。**グループもラベルも無い、ラジオ1つだけ。**
*
* この形は**実際には使い道が無い。** 同じ `name` の相手がいないと、
* 一度入れたものを外せない——ラジオには外す操作が無いためである。
*
* 置けてしまうことを隠さずに出しておく。
*/
export default function Empty() {
return ;
}
```
### 例: edge
```tsx
import { Field } from '../../field/field.tsx';
import { Radio } from '../radio.tsx';
import { RadioGroup } from '../radio-group.tsx';
/**
* エッジケース。**押せない選択肢と、グループに対する誤り。**
*
* 誤りは**グループに付く。** 選ばれていないのは選択肢1つの問題ではないので、
* 1つずつに赤い線を引いても、どれを直せばよいか伝わらない。
*/
export default function Edge() {
return (
);
}
```
# Select (/docs/components/select)
## 素の select ではありません [#素の-select-ではありません]
素のものは**一覧の見た目を OS が描く**ので、揃えられません。
揃える代わりに、キーボード・フォーカス・位置決めをこちらで持っています。
そのため **Select はクライアントコンポーネントです**(`'use client'`)。
開閉の状態とキーボード操作を自分で持つためです。
## 値はフォームにそのまま載ります [#値はフォームにそのまま載ります]
見えている部分は `button` と `ul` で、**値を持っていません。**
値は隠した素の `select` が持ちます。
選んだときは**本物の change を投げます。** 投げないと、素のフォームには載るのに
`onChange` が来ません——`register()` は値の変化を知らないままになります。
これは**テストが測っています。**
```tsx
```
`react-hook-form` なら `{...register('plan')}` をそのまま渡せます。
## フォーカスは開くボタンから動きません [#フォーカスは開くボタンから動きません]
選択肢にフォーカスを移すと、開閉のたびにフォーカスの行き先を管理することになります。
フォーカスは開くボタンに置いたまま、**いまどれを指しているかは `aria-activedescendant`**
で伝えています。
そのぶん、**いま指しているものの印は自分で描いています**——
ブラウザは何も描いてくれないので、無いと矢印キーで動かしても
画面上は何も起きていないように見えます。
## キーボード [#キーボード]
| キー | すること |
| ----------------------- | ------------------- |
| `↓` `↑` `Enter` `Space` | 開く |
| `↓` `↑` | 動かす(**押せないものは飛ばす**) |
| `Home` `End` | 端へ飛ぶ |
| `Enter` `Space` | 選んで閉じる |
| `Escape` | 閉じる |
| 文字 | 打った文字で探す |
**打った文字で探す操作は、かな漢字変換を通す文字では効きません。**
変換中の打鍵は文字として届かないためです。素の `select` も同じです。
## 一覧は箱の中に描かれます [#一覧は箱の中に描かれます]
`body` へ飛ばしていません。飛ばすと**面の入れ子から外れます。**
代償として、**`overflow: hidden` を持つ祖先があると一覧が切り取られます。**
このページのデモでも、囲みの要素が一覧を切り取っていたので外してあります。
### 例: default
```tsx
import { Field } from '../../field/field.tsx';
import { Select } from '../select.tsx';
const plans = [
{ value: 'small', label: 'ふつう' },
{ value: 'large', label: '大きい' },
{ value: 'huge', label: 'とても大きい' },
];
/**
* 通常。**素の `select` ではない。**
*
* 一覧の見た目を揃えるために自前で描いている。
* 値は隠した素の `select` が持つので、フォームにはそのまま載る。
*/
export default function Default() {
return (
);
}
```
### 例: empty
```tsx
import { Field } from '../../field/field.tsx';
import { Select } from '../select.tsx';
/**
* 空。**選択肢が1つも無い。**
*
* 読み込みの前や、絞り込んだ結果が空になる場面がある。
* **開けないようにはしていない**——押せるのに何も起きないと、
* 壊れているのか空なのか区別が付かない。開いて「無い」と伝える。
*/
export default function Empty() {
return (
);
}
```
### 例: edge
```tsx
import { Field } from '../../field/field.tsx';
import { Select } from '../select.tsx';
const many = [
{ value: 'a', label: 'あんず' },
{ value: 'b', label: 'いちご' },
{ value: 'c', label: 'うめ', disabled: true },
{ value: 'd', label: 'えのき' },
{ value: 'e', label: 'おれんじ' },
];
/**
* エッジケース。**押せない選択肢・押せない本体・誤り・とても長いラベル。**
*
* 押せない選択肢は矢印キーで飛ばされる。**止まれてしまうと、
* 選べないものを選ぼうとして何も起きない。**
*/
export default function Edge() {
return (
);
}
```
# Separator (/docs/components/separator)
## 飾りが既定です [#飾りが既定です]
**多くの区切りは見た目のためだけにあります。**
読み上げに出すと「区切り 区切り」と数えられるだけで、意味が増えません。
意味のある区切り(話題が変わる、群が変わる)のときだけ `decorative={false}` にしてください。
```tsx
{/* 飾り。読み上げに出ません */}
{/* 意味のある区切り */}
```
**見た目は変わりません。** 変わるのは読み上げに出るかどうかだけです。
## 縦向きは親の高さに広がります [#縦向きは親の高さに広がります]
**親が高さを持たないと見えません。** 横並びの中で使うときは、
親に高さがあるか `align-items: stretch` になっているか確かめてください。
```tsx
左
右
```
## `hr` ではありません [#hr-ではありません]
縦の区切りに `hr` は意味が合いません。**向きで要素が変わる形にしていません。**
読み上げへの意味は `role` で与えています。
## 太さは1段だけです [#太さは1段だけです]
罫線の太さは1つしか持っていません。**強い区切りは線では表せません。**
区切りの強さは**間隔で表してください。** 線を太くすると、区切りではなく
**枠**として読まれます。
## 要素は差し替えられません [#要素は差し替えられません]
線は**この要素自身の境界**で描いています。子へ移すと、子が持つ境界の既定と重なります
(`hr` は上辺に 1px を持ちます)。
メニューの区切りのように `li` にしたいときは、**外側で包んでください。**
### 例: default
```tsx
import { Separator } from '../separator.tsx';
/**
* 通常。**横向きが既定で、飾りとして扱われる。**
*
* 多くの区切りは見た目のためだけにあるので、読み上げには出さない。
*/
export default function Default() {
return (
);
}
```
### 例: empty
```tsx
import { Separator } from '../separator.tsx';
/**
* 空。**中身を持たない。**
*
* 子を受け取らないので、空の状態はこれ1つしかない。
* 線そのものが中身である。
*/
export default function Empty() {
return ;
}
```
### 例: edge
```tsx
import { Separator } from '../separator.tsx';
/**
* エッジケース。**縦向きは親の高さに広がる。**
*
* 親が高さを持たないと**見えない。** 下の2つを見比べられる。
*
* 意味のある区切り(話題が変わる、群が変わる)のときだけ飾りを外す。
* 見た目は変わらない——**変わるのは読み上げに出るかどうかだけ**である。
*/
export default function Edge() {
return (
);
}
```
# Spinner (/docs/components/spinner)
## 回転はトークンが持っています [#回転はトークンが持っています]
スピナーの形と大きさだけがこのコンポーネントのもので、**回転はトークン層にあります。**
`data-sg-spinner` を付けた要素が回るので、**素の CSS の利用者も同じものを得られます。**
周期はループのスケールから来ています。**生の数値は書いていません。**
## 動きだけで状態を伝えません [#動きだけで状態を伝えません]
**動きを減らす設定(`prefers-reduced-motion`)では止まります。** スケルトンと同じ判断です。
止まったスピナーからは進行が読み取れないので、**名前を型で必須にしてあります。**
```tsx
```
すでに名前を持つものの中に置くときは、逆に**隠さないと二重に読まれます。**
```tsx
読み込み中
```
`aria-label` / `aria-labelledby` / `aria-hidden` のどれか1つが必ず要ります。
## 色は継承します [#色は継承します]
スピナーの色は**文字の色をそのまま使います。** 塗りの上でも凹んだ面の上でも、
置いた場所の前景に従うので書き直す必要がありません。
## 大きさは行の高さに合わせてあります [#大きさは行の高さに合わせてあります]
文字やアイコンと横に並べたときに揃います。**段は1つだけです。**
### 例: default
```tsx
import { Spinner } from '../spinner.tsx';
/**
* 通常。**回転はトークン層が持っている。**
*
* スピナーの色は文字の色をそのまま使う。置いた場所の前景に従うので、
* 面が変わっても色を書き直す必要が無い。
*/
export default function Default() {
return (
読み込み中
);
}
```
### 例: empty
```tsx
import { Spinner } from '../spinner.tsx';
/**
* 空。**中身を持たない。**
*
* 子を受け取らないので、空の状態はこれ1つしかない。
* 名前だけは必ず要る——**止まったときに見た目から進行が読み取れない**ため。
*/
export default function Empty() {
return ;
}
```
### 例: edge
```tsx
import { Spinner } from '../spinner.tsx';
/**
* エッジケース。**前景の色が変わる場所に置く。**
*
* スピナーは文字の色をそのまま使うので、淡い塗りの上でも凹んだ面の上でも
* 書き直さずに読める。
*
* 動きを減らす設定では**止まる。** 止まったスピナーからは進行が読み取れないので、
* 名前を型で必須にしてある。
*/
export default function Edge() {
return (
);
}
```
# Switch (/docs/components/switch)
## チェックボックスとの違い [#チェックボックスとの違い]
**押した瞬間に効くもの**がスイッチです。
チェックボックスは**送信して初めて効きます。**
見た目だけの違いではないので、コンポーネントを分けています。
迷ったときは「押したら何か起きるか」で決めてください。
## 素の input に乗っています [#素の-input-に乗っています]
`type="checkbox"` に `role="switch"` を足しただけです。
**入ったかどうかの申告も、キーボードも、フォームへの載り方もブラウザが持っています。**
自分で状態を持っていないので、素のフォームでも `register()` でもそのまま載ります。
## つまみは境界で形を保ちます [#つまみは境界で形を保ちます]
つまみは、**入っているときアクセントの塗りの上、切れているとき凹んだ面の上**に載ります。
地の色が明暗の両極なので、**塗りだけでは片方でつまみが背景に埋もれます。**
境界が識別を担っています。
## ラベルは Field が付けます [#ラベルは-field-が付けます]
何の入り切りかは、スイッチ自身からは分かりません。
```tsx
```
## 押せないときは塗りの段が落ちます [#押せないときは塗りの段が落ちます]
**不透明度では表しません。** 薄めると前景と背景が同時に下地へ寄り、
読みやすさの保証が効かなくなります。
### 例: default
```tsx
import { Field } from '../../field/field.tsx';
import { Switch } from '../switch.tsx';
/**
* 通常。**押した瞬間に効くもの**に使う。
*
* 送信して初めて効くものはチェックボックスである。**見た目だけの違いではない。**
*/
export default function Default() {
return (
);
}
```
### 例: empty
```tsx
import { Field } from '../../field/field.tsx';
import { Switch } from '../switch.tsx';
/**
* 空。**切れているとき。**
*
* つまみは境界を持つので、地に沈まない。**塗りだけだと、
* 凹んだ面の上で見えなくなる。**
*/
export default function Empty() {
return (
);
}
```
### 例: edge
```tsx
import { Field } from '../../field/field.tsx';
import { Switch } from '../switch.tsx';
/**
* エッジケース。**押せないときと、誤りのとき。**
*
* 押せないときは塗りの段が落ちる。**不透明度では表さない。**
*/
export default function Edge() {
return (
);
}
```
# Table (/docs/components/table)
## 横にはみ出しても、スクロールするのは表だけです [#横にはみ出してもスクロールするのは表だけです]
**列が増えると、表は必ず横にはみ出します。** そのとき横スクロールする枠が要りますが、
**`Table` がその枠を持っています。** 利用側は書きません。
利用側に書かせると、書き忘れたときに**ページ全体が横にスクロールします。**
## `thead` と `tbody` は素のまま書きます [#thead-と-tbody-は素のまま書きます]
どちらにも当てるものがありません。**中身の無いコンポーネントは置いていません。**
```tsx
```
## 数字の列は `numeric` を付けます [#数字の列は-numeric-を付けます]
**右に寄り、桁の幅が揃います。** 揃えないと、上下の行で桁の位置がずれて
読み比べられません。
見出しの項目にも付けてください。**中身と見出しが別の側に寄ると読みにくくなります。**
## 見出しは申告されます [#見出しは申告されます]
`TableHeaderCell` は既定で**列の見出し**として申告します。
行の見出しにするときは `scope="row"` を渡してください。
申告しないと、読み上げが**どの項目がどの見出しに属するかを言えません。**
## 表そのものは素の `table` です [#表そのものは素の-table-です]
役割を付け替えていません。**素の `table` が既に表として読まれます。**
### 例: default
```tsx
import { Table, TableCell, TableHeaderCell, TableRow } from '../table.tsx';
/**
* 通常。**数字の列は右に寄り、桁の幅が揃う。**
*
* 揃えないと、上下の行で桁の位置がずれて読み比べられない。
*
* `thead` と `tbody` は素のまま書く。どちらにも当てるものが無いので、
* **枠だけのコンポーネントを置いていない。**
*/
const rows = [
{ name: '5km', pace: '4:52', count: 128 },
{ name: '10km', pace: '5:04', count: 41 },
{ name: 'ハーフ', pace: '5:31', count: 7 },
];
export default function Default() {
return (
距離帯
最速ペース
本数
{rows.map((r) => (
{r.name}
{r.pace}
{r.count}
))}
);
}
```
### 例: empty
```tsx
import { Table, TableCell, TableHeaderCell, TableRow } from '../table.tsx';
/**
* 空。**行が無くても見出しは残る。**
*
* 表そのものを消すと、何を待っているのかが分からなくなる。
* 見出しを残したうえで、**空であることを1行で言う。**
*
* その1行は列をまたぐ。`colSpan` は素の属性なので、そのまま渡せる。
*/
export default function Empty() {
return (
);
}
```
### 例: edge
```tsx
import { Table, TableCell, TableHeaderCell, TableRow } from '../table.tsx';
/**
* エッジケース。**列が多くて横に溢れるとき。**
*
* 溢れたら**表の中だけが横に動く。** ページは動かない——
* 横スクロールする枠を利用側に書かせていないためである。
*
* 行の見出しを左端に置く形も見る。`scope="row"` を渡すと、
* 読み上げが**どの項目がどの見出しに属するか**を言えるようになる。
*/
const cols = ['距離', 'タイム', 'ペース', '心拍', '高度', '気温', '日付', '本数'];
export default function Edge() {
return (
回
{cols.map((c) => (
{c}
))}
{[1, 2].map((n) => (
{n} 回目
{cols.map((c) => (
1234.5
))}
))}
);
}
```
# Tabs (/docs/components/tabs)
## 依存は持ちません [#依存は持ちません]
素の HTML にタブはありませんが、**作るのに要るのはブラウザが持っているものだけ**です——
`button` と、フォーカスの移動と、キーボードの操作。
## Tab キーで止まるのは選択中のタブだけです [#tab-キーで止まるのは選択中のタブだけです]
選択中のタブだけが Tab キーの移動先になります。**全部が対象だと、
タブの数だけ Tab キーを押すことになります。**
並びの中は矢印キーで移ります。`Home` で先頭、`End` で末尾。
**端では折り返します**——端から端へ戻るのに全部通らずに済みます。
## 矢印キーで移動すると、その場で中身が切り替わります [#矢印キーで移動するとその場で中身が切り替わります]
`activation="manual"` を渡すと、**押して初めて開きます。**
| | いつ |
| --------------- | ----------------------------------------------------- |
| `automatic`(既定) | **中身がその場にある**とき。開くのに待ちがない |
| `manual` | **中身の読み込みに時間がかかる**とき。すぐ開く形だと、矢印キーで通り過ぎるだけで毎回読み込むことになる |
## 押せないタブは飛ばされます [#押せないタブは飛ばされます]
矢印キーの移動先にもなりません。**止まれるのに開けないのは筋が通りません。**
## 名前は利用側が渡します [#名前は利用側が渡します]
`id` から `aria-controls` と `aria-labelledby` を作ります。
**同じ画面に2つ置くときは別の名前を渡してください。**
Tabs が作って配る形にはしていません。**利用側が中身を別の場所から指したいとき**に
名前が分からなくなるためです。
## 選択していないタブの中身も DOM に置かれます [#選択していないタブの中身も-dom-に置かれます]
表示していないだけで、すべてのタブの中身が最初から DOM にあります。
**ブラウザのページ内検索で見つかり、印刷にも出ます。**
重いコンポーネントを選ばれるまで描きたくない場合は、
**中身の側で出し分けてください。** Tabs は出し分けを持ちません。
## 選択中のタブは、色以外でも見分けられます [#選択中のタブは色以外でも見分けられます]
下線と文字の濃さで示します。読み上げには `aria-selected` で伝わります。
**色が見分けられない環境でも、どれが選ばれているか分かります。**
### 例: default
```tsx
import { Tabs, TabsList, TabsPanel, TabsTrigger } from '../tabs.tsx';
/**
* 通常。**矢印キーで移ると、移った先がすぐ開く。**
*
* フォーカスが入るのは選ばれているものだけである。**全部が tab 順に入ると、
* タブの数だけ Tab キーを押すことになる。**
*/
export default function Default() {
return (
表示
通知
アカウント
画面の見え方を変えます。
どんなときに知らせるかを決めます。
名前とパスワードを変えます。
);
}
```
### 例: empty
```tsx
import { Tabs, TabsList, TabsPanel, TabsTrigger } from '../tabs.tsx';
/**
* 空。**中身が無いとき。**
*
* タブは残り、中身の場所も残る。**枠ごと消えると、
* 何かが失敗したのか、もともと無いのかが分からない。**
*/
export default function Empty() {
return (
すべて
まだ何もありません。
);
}
```
### 例: edge
```tsx
import { Tabs, TabsList, TabsPanel, TabsTrigger } from '../tabs.tsx';
/**
* エッジケース。**押せないタブと、押して初めて開く形。**
*
* 押せないタブは矢印キーでも飛ばされる。**止まれるのに開けないのは嘘である。**
*
* `manual` は押して初めて開く。**中身の読み込みに時間がかかるとき**に使う——
* すぐ開く形だと、矢印キーで通り過ぎるだけで毎回読み込むことになる。
*/
export default function Edge() {
return (
使える
使えない
これも使える
1つめの中身
2つめの中身
3つめの中身
1つめ
2つめ
矢印キーで移っても、押すまで開きません。
こちらも同じです。
);
}
```
# Textarea (/docs/components/textarea)
## 1行の入力欄と同じ見た目です [#1行の入力欄と同じ見た目です]
違うのは高さだけです。**同じものを使っています**——
別に書くと、片方だけ直したときにずれます。
高さが一致していないことは**テストが測っています。**
## 行数は利用側が決めます [#行数は利用側が決めます]
`rows` を渡してください。**何行が適切かは中身の事情**であって、
システムの事情ではありません。
### 例: default
```tsx
import { Field } from '../../field/field.tsx';
import { Textarea } from '../textarea.tsx';
/**
* 通常。**凹んだ面を宣言している。**
*
* 入力欄は地であって塗りではない。背景と文字が対で決まるので、塗るだけの道が無い。
*
* ラベルとの結びつけは Field が作る。単体でも置けるが、
* **そのときは結びつけを利用側が書くことになる。**
*/
export default function Default() {
return (
);
}
```
### 例: empty
```tsx
import { Textarea } from '../textarea.tsx';
/**
* 空。**中身もラベルも無いとき。**
*
* 単体で置くとラベルとの結びつけが無い。**読み上げは「編集欄」としか言わない**ので、
* 実際には Field と一緒に使う。ここでは枠そのものの見た目を見る。
*/
export default function Empty() {
return (
);
}
```
### 例: edge
```tsx
import { Field } from '../../field/field.tsx';
import { Textarea } from '../textarea.tsx';
/**
* エッジケース。**誤りのときと、押せないとき。**
*
* 誤りの境界は mark の段を使う。**文字ではないものに要る 3:1 を満たす段**で、
* 役割としても合っている。
*
* 押せないときは文字が淡くなる。**不透明度では表さない。**
*/
export default function Edge() {
return (
);
}
```
# Toast (/docs/components/toast)
## どこからでも出せます [#どこからでも出せます]
トーストの状態は **React の外**で持っています。深い階層からでも、React の外
(読み込みの失敗処理など)からでも、`showToast()` をそのまま呼べます。
```tsx
showToast({ message: '保存しました', tone: 'success' });
```
コンテキストでは配っていません。配ると、**呼び出す場所がすべて Provider の内側に
なければならず**、React の外からは出せなくなります。
出ているトーストを読みたいときは `useToast()` を使います。出すだけなら `showToast()` で足ります。
`` を**アプリに1つだけ**置いてください。
## 読み上げの領域は、文言より先に描かれています [#読み上げの領域は文言より先に描かれています]
1つも出ていないときも領域だけは描いています。**後から領域ごと現れると、
読み上げは中身の追加に気づきません。**
見た目には何も出ない誤りなので、テストが測っています。
## 調子は4つです [#調子は4つです]
| 調子 | 使うとき |
| --------- | ------------------- |
| `default` | 起きたことをそのまま伝える |
| `success` | 求めた操作が全部済んだ |
| `warning` | **済んだが、求めたとおりではない** |
| `danger` | できなかった |
`warning` は「在庫が足りず一部だけ入った」「保存はしたが同期は後で」のようなときに使ってください。
`danger` で出すと**取り消しや再試行を促す文面と混ざり**、`default` で出すと**求めたとおり済んだ場合と見分けが付きません。**
`info` は持っていません。知らせは作業の結果を伝えるものなので、`default` と役割が重なります。
## すぐに気づいてほしい内容には向きません [#すぐに気づいてほしい内容には向きません]
読み上げの領域は1つだけで、`polite`(読み終わるのを待つ)です。
**重要度で領域を分けると、重要なものが上・そうでないものが下と並び順が固定され、
出た順に読めなくなります。**
読み上げに割り込んででも伝えたい内容は、トーストではなく **Modal** を使ってください。
## 表示時間の既定は4秒です [#表示時間の既定は4秒です]
何も渡さなければ 4秒で消えます。この値は**トークンの滞在時間スケール**から来ていて、
コンポーネントに数値を直接書いていません。
**3秒を下回ると読み切れません。** 長い文言や、読んでから操作するものは
スケールの長い側(5.7秒・8秒)を渡してください。10秒を超えると、
気づかないまま消えたのか出ていないのかが分からなくなります。
読み終わるまで残したいものは `duration: null` を渡してください。
**ポインタが乗っている間は消しません。** 離れたらもう一度はじめから数えます——
残り時間を持つと時計が2箇所に増えるためで、**長く出る側へ倒れます。**
トークンを入れていない環境ではスケールを読めません。そのときは**消さない側に倒します**
——適当な数値を書くと、スケールに無い値が1つ増えてしまうためです。
## `` はアプリに1つだけ置いてください [#toaster--はアプリに1つだけ置いてください]
2つ置いても**先に置いた方しか描かれません。** 2つ目は何もしません。
そうしているのは、両方が描くと**読み上げの領域が2つになり、同じ文言が2回読まれる**
ためです。先に置いた方が外れたときは、残っている方が引き継ぎます。
## 最前面の層に出ます [#最前面の層に出ます]
`overflow` を持つ祖先があっても切り取られません。枠は何も塗らず押せないので、
**後ろのページはそのまま押せます。**
## 残り時間をバーで示します [#残り時間をバーで示します]
トーストの下にバーが出て、**満ちた状態から空へまっすぐ減ります。**
加減速は付けていません——**時間そのものを表しているので、速さが変わると嘘になります。**
ポインタを乗せているあいだは止まります。**外すと、止めた続きから数え直します。**
`duration: null` を渡したものには出ません。**消えないものに残り時間はありません。**
**動きを減らす設定(`prefers-reduced-motion`)でも止まりません。** これは装飾ではなく、
あと何秒で消えるかという情報です。止めると読み取れなくなります。
読み上げには出しません。**時間の経過を読ませると、文言が繰り返し流れます。**
## 表示と消去にアニメーションが付きます [#表示と消去にアニメーションが付きます]
現れるときは薄れながら少し上がり、消えるときは**薄れながら高さが縮みます。**
高さを縮めるのは、**後ろに並んでいるトーストが跳ねて動かないようにする**ためです。
閉じてもその場では消えません。**アニメーションが終わってから DOM から外れます。**
**動きを減らす設定ではアニメーションが止まります。** これは装飾なので、
無くても内容は伝わります。ただし閉じたトーストはすぐに外れます——
止めた結果いつまでも残ってしまうと困るためです。
## 残り時間を数えている場所は1つだけです [#残り時間を数えている場所は1つだけです]
消えるタイミングは**バーのアニメーションそのもの**が決めています。
別にタイマーを持っていません。バーが止まれば消えるのも止まり、
再開すれば続きから進みます。**2つがずれることがありません。**
### 例: default
```tsx
'use client';
/*
**例そのものをクライアント側にする。** Toaster はクライアント側のコンポーネントで、
出す操作(関数)もサーバ側からは渡せない。
*/
import { Button } from '../../button/button.tsx';
import { Toaster } from '../toast.tsx';
import { showToast } from '../toast-store.ts';
/**
* 通常。**出すのはただの関数呼び出し。**
*
* 置き場は React の外にあるので、深いところからでも
* React の外(読み込みの失敗処理など)からでも呼べる。
*/
export default function Default() {
return (
{/* **済んだが、求めたとおりではない。** 失敗ではないので danger にしない */}
);
}
```
### 例: empty
```tsx
'use client';
/*
**例そのものをクライアント側にする。** Toaster はクライアント側のコンポーネントである。
*/
import { Toaster } from '../toast.tsx';
/**
* 空。**1つも出ていない。**
*
* このとき何も見えないが、**読み上げの領域は描かれている。**
* 後から領域ごと現れると、読み上げは中身の追加に気づかない。
*/
export default function Empty() {
return ;
}
```
### 例: edge
```tsx
'use client';
/*
**例そのものをクライアント側にする。** Toaster はクライアント側のコンポーネントである。
*/
import { Button } from '../../button/button.tsx';
import { Toaster } from '../toast.tsx';
import { showToast } from '../toast-store.ts';
/**
* エッジケース。**消えないものと、長い文言と、積み重なり。**
*
* 既定は滞在の段のまん中(4000ms)である。読み終わるまで残したいものは
* `duration: null` を渡す。**ポインタが乗っている間は消えない。**
*/
export default function Edge() {
return (
);
}
```