メインコンテンツへスキップ
menumenu tanreiexpand_morecontrasttranslate
tanreiexpand_more
はじめにWebReactSwiftUIJetpack ComposeFlutterReact Nativeプリセット自分のデザインシステムを作るAI エージェントと Tanrei を使うブランドデザイントークンデザインシステムとカスタマイズTheme Studioカラータイポグラフィアイコン形状エレベーションモーションインタラクション状態振る舞いレイアウトアクセシビリティ言葉カラートークンタイプスケールトークン形状トークンエレベーショントークンモーショントークン状態トークンスペースと寸法のトークンコンポーネントボタンメインアクションボタンセグメントボタンボタングループメニュー付きボタンツールバーコピーボタンチェックボックスラジオボタンスイッチチップメニュースライダースライダーフィールドカレンダーDateTimePickerフィールドグループラジオグループチェックボックスグループセレクトセレクトフィールドメニューバーファイルアップロードファイルピッカー 上級者向けドロップゾーンテキスト入力欄 上級者向けテキストフィールドチップフィールドフィールドコンボボックス 上級者向けコンボボックスフィールド数値入力欄 上級者向け数値フィールドカラー入力欄 上級者向けカラーフィールドワンタイムコード入力欄 上級者向けワンタイムコードフィールド埋め込みテキスト入力欄 上級者向けチャットコンポーザーカード区切り線モーダルレイヤー 上級者向けダイアログリストリスト行ボトムシートサイドシートカルーセルコードブロックテーブルポップオーバーステップアコーディオン説明リスト図アプリシェルページヘッダーツリービュータイムライン画像プレビューハンドルサイズ変更できる枠スクロールエリアバッジプログレスインジケーターメーターナンバーティッカーローディングインジケータースナックバーツールチップキーボードショートカットタグインラインコードコールアウトアバター空の状態タググループスケルトンファイル項目色見本マーキーテキストタブタブビューアプリバーナビゲーションバーナビゲーションドロワー検索リンクページネーション前後のリンクステッパーパンくずリスト目次リンクカードFAB メニューステータスTanrei の作り方コントリビューションライセンスと帰属
ホームはじめにWeb

Web

Tanrei の CSS トークンと Web Components を、どの Web プロジェクトでも使えます。

content_copyMarkdown で表示

@tanrei/web には 2 つのものが入っています。デザイントークンを CSS カスタムプロパティとして収めたスタイルシートと、Lit で作った標準準拠のカスタム要素です。特定のフレームワークは必要ありません。

プレリリース

0.1.0-next.0 を npm の dist-tag next で公開しています。0.1.0 までは @tanrei/web@next をインストールしてください。0.x のプレリリースのあいだは後方互換を保ちません。どのプレリリースも、先に非推奨の期間を置くことなく何でも壊すことがあり、何を壊したかはその changeset に書いてあります。

インストール

npm から

npm install @tanrei/web

ソースから

公開される前の main のソースを使うには、Git、Node 22.22 以降、pnpm 12 が必要です(corepack enable で、リポジトリが固定しているバージョンが使えます)。リポジトリのクローンでパッケージをビルドしてパックし、パックしたファイルをプロジェクトにインストールします。

git clone https://github.com/chnotchy/tanrei.git
cd tanrei
pnpm install
pnpm --filter @tanrei/web build
pnpm --filter @tanrei/web pack --pack-destination ../tanrei-packages
# プロジェクトで:パッケージを書き出したフォルダーへのパス
npm install path/to/tanrei-packages/tanrei-web-*.tgz

パックしたファイルには、リリースで公開するものがそのまま入っているので、以下の手順はすべて同じように動きます。ソースの新しい状態を取り込むには、pull してからビルドとパックをやり直し、新しいファイルをインストールします。

使い方

  1. スタイルシートを一度だけ、できるだけ早い段階で読み込みます。どれもカスケードレイヤー(tanrei.*)に包まれているので、自分のスタイルが常に優先されます。

    import '@tanrei/web/tanrei.css';

    tanrei.css は、すべてを 1 つにまとめたファイルです。ページのベース、トークン、要素なしで描くパーツ(テキストスタイル、テーブル、アコーディオンなど)のスタイルシートが入っています。使うものだけを読み込むなら、トークンと、必要ならベースを読み込み、それらのスタイルシートは要るものだけを足します。

    import '@tanrei/web/tokens.css';
    import '@tanrei/web/base.css'; // ページをシステムのサーフェス、文字色、本文の書体にし、body の余白をなくす

    ベースは、ページそのものにサーフェスの色を、自分のテキストに on-surface の色と本文の書体を与え、ブラウザーが body に付ける余白を取り除きます。ベースがなければ、ページはブラウザーの既定のままです。

  2. 自分のデザインシステムのコンポーネントを登録します。使う要素だけをインポートすることもできます。

    import '@tanrei/web';
    // プリセットでは、代わりにそのエントリーを: m3-like、m3-expressive-like、cupertino
    import '@tanrei/web/m3-like';
    // または、要素を 1 つずつ:
    import '@tanrei/web/components/button';

    @tanrei/web が登録するのは既定のシステムが持つコンポーネントだけなので、その上のプロダクトから、システムにないコンポーネントには手が届きません。プリセットのエントリー @tanrei/web/<id> はそのプリセットが持つものを、@tanrei/web/all はパッケージのすべてのコンポーネントを登録します。セグメントボタンのようにプリセットにしかないコンポーネントも、意図して使うことはできます。自分のシステムのレイヤーで取り込み、そのコンポーネント自身のモジュールを読み込みます。

    @tanrei/web/… という指定子はパッケージのエクスポートで、バンドラー(Vite、webpack、esbuild)が解決します。バンドラーがない場合、ブラウザーは <link> や <script src> の中の裸の指定子を解決できません。そのときはパッケージの dist/ を配信して、ファイルを直接指定します。スタイルシートはそのまま、スクリプトは Lit とすべての要素を含む tanrei.bundle.js を使います。

    <link rel="stylesheet" href="/node_modules/@tanrei/web/dist/tanrei.css" />
    <script type="module" src="/node_modules/@tanrei/web/dist/tanrei.bundle.js"></script>
  3. 要素を、ほかの HTML と同じように使います。

    <tanrei-button variant="filled">デプロイ</tanrei-button>
    <tanrei-button variant="outlined" size="small" disabled>キャンセル</tanrei-button>
  4. プロダクトの言葉を渡します。コンポーネントは自分の言葉を持ちません。必須・任意のタグのテキストや閉じるボタンの名前などは、プロダクトのものです。まずはすべてを平易な英語にするなら、英語の言葉のパックを一度だけ import します。

    import '@tanrei/web/words/en';

    どれかを変えるとき、または別の言語の言葉を渡すときは、そのあとで setWords() を呼びます。渡した言葉はパックの言葉に優先し、要素に書いた言葉はその両方に優先します。

    import { setWords } from '@tanrei/web';
    
    setWords({ requiredText: 'Mandatory' });
    setWords({ requiredText: '必須', optionalText: '任意', closeLabel: '閉じる' }, { lang: 'ja' });

    パックも自分の言葉もなくてもコンポーネントは動作し、どの言葉が足りないかをコンソールで知らせます。言葉を参照してください。

ライブ

デプロイキャンセル

要素のマニフェスト

エディターやコーディングエージェントは、パッケージの要素を Custom Elements Manifest の @tanrei/web/custom-elements.json から読みます。すべての要素の属性、プロパティ、イベント、スロット、パーツが載っていて、プロダクトから触れないものは載っていません。@tanrei/web/custom-elements/<id>.json にはシステム <id> が持つ要素だけが載っているので、1 つのシステムで使うツールはそれ以外を提案しません。

テーマとコントラスト

トークンの値は light-dark() を使っているので、使われている要素の color-scheme に従います。何も設定しなければ OS の設定に従い、prefers-contrast: more のときは高コントラストのスキームに切り替わります。

自分で決めたいときは、属性を設定します。

<!-- ページ全体、または任意のサブツリーのテーマを固定する -->
<html data-tanrei-theme="dark">

<!-- ページのコントラストレベルを固定する:"medium" または "high" -->
<html data-tanrei-contrast="high">

ノート

どちらの属性も、<html> にも任意のサブツリーにも使えます。生成されるスタイルシートは、トークン一式を :root, [data-tanrei-contrast] に宣言しています。そのため、入れ子の要素に data-tanrei-contrast を付けると、システムの色だけでなく、その中のコンポーネントトークンも、そのコントラストレベルで解決し直されます。

選んだテーマを保つ

プロダクトでテーマを選べるなら、選んだものを保存し、ページが最初に描かれる前に <html> に付けます。あとから動くスクリプトで付けると、ページは最初の 1 フレームだけシステムのテーマで表示されます。@tanrei/web/theme-script はその両方をします。themeScript() は <head> のスタイルシートより前にインラインで書く小さなスクリプトで、saveTheme() は選んだテーマを保存してすぐに描きます。

import { loadTheme, saveTheme } from '@tanrei/web/theme-script';

saveTheme('dark'); // 'light'、'dark'、または OS に従い直す 'system'
loadTheme();       // 保存されているもの: 'light'、'dark'、'system'

Next.js(App Router)では、ルートのレイアウトにインラインで書きます。suppressHydrationWarning を付けるのは、React がハイドレートする前にスクリプトが <html> の属性を変えるからです。

// app/layout.tsx
import { themeScript } from '@tanrei/web/theme-script';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head>
        <script dangerouslySetInnerHTML={{ __html: themeScript() }} />
      </head>
      <body>{children}</body>
    </html>
  );
}

Astro では、レイアウトにインラインで書きます。

---
// src/layouts/Layout.astro
import { themeScript } from '@tanrei/web/theme-script';
---
<html lang="en">
  <head>
    <script is:inline set:html={themeScript()} />
  </head>
  <body><slot /></body>
</html>

自分の CSS でトークンを使う

すべてのトークンは、パスにちなんだ名前のカスタムプロパティです。tanrei.sys.color.primary は --tanrei-sys-color-primary になります。

.panel {
  background: var(--tanrei-sys-color-surface-container-low);
  border-radius: var(--tanrei-sys-shape-corner-medium);
  box-shadow: var(--tanrei-sys-elevation-level1);
  font: var(--tanrei-sys-typescale-body-font);
}

スクリプトからトークンを設定するには、@tanrei/web の setTokens() を使います。ルートなら setTokens({ '--tanrei-sys-color-primary': '#0b57d0' })、ページの一部なら { on: element } を付けます。ルートにトークンを 1 つだけ style.setProperty() で設定すると、Safari はページ全体のスタイルを数秒かけて計算し直します。setTokens() は、そうならないように書き込みます。

テキストスタイル

テキストスタイルは階層で名付けられています(heading-2 は h2、body は p、label はコントロール上のテキスト)。それぞれが、ひとまとまりのカスタムプロパティです。ショートハンドの --tanrei-sys-typescale-<role>-font に加えて、-font-family、-font-size、-font-weight、-line-height、-letter-spacing、-font-weight-emphasized があります。次の 2 つの任意のスタイルシートを使うと、これらを要素に適用できます。

import '@tanrei/web/text.css';   // .tanrei-text-<role>、.tanrei-text-emphasized
import '@tanrei/web/prose.css';  // .tanrei-prose の中の h1–h6、p、small、code、pre
<h2 class="tanrei-text-heading-2">セクション</h2>
<p class="tanrei-text-body">本文の中の<strong class="tanrei-text-emphasized">強調</strong>。</p>
<article class="tanrei-prose"><!-- レンダリングされた Markdown --></article>

.tanrei-text-emphasized は、置かれているロールを、その太さにファミリーの強調のステップを足した太さで描きます。これは太さであって、別のロールではありません。16 のロールと要素との対応は、タイポグラフィにあります。

フォント

タイプスケールは、Inter、Noto Sans JP、Noto Sans、Roboto Mono を指定します(tokens.css の --tanrei-ref-typeface-{brand,plain,mono}。tanrei build は、どのシステムについてもこの一覧を出力します)。アイコンには Material Symbols Rounded を指定し、いずれもシステムフォントにフォールバックします。Tanrei はフォントファイルを同梱しません。ちょうどこれらのファミリー名で、自分で読み込んでください。Google Fonts から読み込むか、@fontsource-variable/* パッケージを使います(これらのパッケージは Inter Variable のような名前で登録するので、このサイトと同じように、素の名前でそのファイルを指す @font-face を自分で宣言します)。

アイコン

import '@tanrei/web/icons.css';
<tanrei-button><span slot="icon" class="tanrei-icon" aria-hidden="true">add</span>追加</tanrei-button>

.tanrei-icon は、Material Symbols の名前を、システムのアイコンの太さで描き、サイズは置かれた場所から取ります。アウトラインか塗りかを自分で選ぶことはありません。アイコンを参照してください。

Material Symbols Rounded のファイルは、すべてのグリフを、すべての太さ・塗り・グレード・オプティカルサイズで持っていて、5.4 MB あります。遅い回線では、ページがこれを 30 秒ほど待つことがあります。読み込むのは、製品が描くグリフだけにしてください。tanrei build は、Tanrei のコンポーネントが自分で描くことのあるグリフの名前(山形、チェック、閉じるボタンの×など)と、そのシステムがアイコンを描く軸の値を表示します。この一覧に、製品自身が使う名前を足してください。Google Fonts から読み込むなら、名前をアルファベット順に並べて icon_names= に渡し、軸は family= に範囲で書きます(既定のシステムでは Material+Symbols+Rounded:opsz,wght,FILL,[email protected],300..500,0..1,0)。ファイルを自分で配信するなら、フォントの合字の機能(rlig、rclt)と両方の塗りを残すサブセッターで間引きます。このサイトのアイコンも、そうして作ったファイルで描いています。どちらの場合も、アイコンのフェイスは font-display: block で登録してください。ファイルが届くまでアイコンは見えないままで、名前が文字として出ることはありません。

ブラウザーの対応

Tanrei は、次のバージョン以降で動きます。

ブラウザー バージョン
Chrome、Edge 125
Firefox 126
Safari(macOS、iOS) 17.5

どの数字も、Tanrei が描くのに欠かせない機能のうち、いちばん新しいものが使えるようになったバージョンです。どの機能にも代わりの手段はありません。

  • CSS: 色はすべて light-dark() の組です(Chrome・Edge 123、Firefox 120、Safari 17.5)。サイズと文字サイズは CSS の round() で丸めています(Chrome・Edge 125、Firefox 118、Safari 15.4)。コンポーネントは、カスタムステート(:state())で見た目を切り替えます(Chrome・Edge 125、Firefox 126、Safari 17.4)。メニュー、ツールチップ、スナックバーは、Popover API で最上位レイヤーに開きます(Chrome・Edge 114、Firefox 125、Safari 17)。スタイルシートを包んでいるカスケードレイヤーは、それより前から使えます(Chrome・Edge 99、Firefox 97、Safari 15.4)。
  • JavaScript: パッケージは ES2022 で、公開するファイルが 1 つでもそれを超えるとビルドが失敗します。いちばん新しい構文はクラスの static ブロックで、Chrome・Edge 94、Firefox 93、Safari 16.4 から動きます。パッケージを古いターゲット向けにトランスパイルしても、下限は下がりません。上の CSS は変わらないからです。

これより新しい機能は、なくても動く場所でだけ使っています。@starting-style がなければ開くときのアニメーションが再生されず、corner-shape がなければ角が丸いままになるだけです。

古いブラウザーで開いた人に知らせる

Tanrei 自身はこれをしません。製品のエントリーポイントの役目です。先に機能を確かめ、そろっているときだけ、アプリを Tanrei ごと読み込みます。

<p id="unsupported" hidden>このサイトには Chrome または Edge 125、Firefox 126、Safari 17.5 以降が必要です。</p>
<script type="module">
  // Safari の下限は light-dark() で、Chrome と Edge の下限は round() で、Firefox の下限は :state() で決まる
  const supported =
    CSS.supports('color', 'light-dark(red, red)') &&
    CSS.supports('width', 'round(1px, 1px)') &&
    CSS.supports('selector(:state(open))');
  if (supported) import('./app.js'); // @tanrei/web を読み込むアプリ
  else document.getElementById('unsupported').hidden = false;
</script>

このスクリプトは小さく、素直に書いてください。古いものも含め、訪れた人のブラウザーがすべてこれを実行します。静的な import にすると、ブラウザーはこのスクリプトより先にアプリを読み込むので、アプリを解析できない古いブラウザーでは確認そのものが動きません。import() なら、確認が済んでから読み込みます。

テスト

ブラウザーで動かす

Tanrei の要素は、ブラウザーが実装している機能の上に作られています。シャドウルート、ElementInternals(フォームコントロールの値と妥当性、カスタムステート)、Popover API、<dialog>、そしてレイアウトです。jsdom と happy-dom はその一部しか実装していません(happy-dom には attachInternals() も Popover API もありません)。そのため、描画した時点で要素が例外を投げたり、拒否された Promise が残ったり、要素が前提にしているサイズやフォーカス順、トップレイヤーがないまま描画されたりします。Tanrei はこれらの環境をサポートしておらず、足りない機能の代わりになるもの(スタブ)も提供していません。要素が例外を投げないようにするだけの代用品を置くと、どのブラウザーにもない振る舞いでテストが通ってしまうからです。

Tanrei の要素を描画するテストは、実際のブラウザーで動かしてください。

  • Vitest のブラウザーモードを Playwright プロバイダーで使えば、コンポーネントのテストを Vitest に置いたまま、Chromium、Firefox、WebKit で動かせます。
  • Playwright Test は、ページや一連の操作の流れを動かします。

Tanrei の要素を描画しない、自前のロジックのテストは、Node や jsdom、happy-dom のままでかまいません。Tanrei 自身のテストは、Chromium、Firefox、WebKit で、Playwright と axe-core を使って動かしています。

要素を探す

ほとんどの要素は、自分自身ではなくシャドウルートの中のネイティブ要素(<button>、<input>、<dialog>)にロールを持たせています。ブラウザーやテストツールに本物のセマンティクスを見せるためです。タブや開いているメニューなど、一部の要素は自分自身がロールを持ちます。Playwright のロケーターは開いたシャドウルートの中まで探すので、人と同じように、ロールと名前でコントロールを探してください。

await page.getByRole('button', { name: '保存' }).click();
await expect(page.getByRole('switch', { name: 'Wi-Fi' })).toBeChecked();

そのボタンのクリックは、<tanrei-button> 自身に登録したリスナーにも届きます。Testing Library のクエリはシャドウルートの中を探さないので、これらのコントロールは見つかりません。

ロールと中身が別のツリーにある場合

要素によっては、ロールを自分のシャドウルートに持ち、その中身はスロットで差し込まれた利用側のマークアップ、という形になっています。<tanrei-tabs> のタブリストとその子の <tanrei-tab>、<tanrei-dialog> の <dialog> とそのアクションがそうです。DOM の上では、タブはタブリストの中にありません。そのため、ロールからつないだロケーターでは何も見つかりません。

// 何も見つからない:タブは <tanrei-tabs> の子で、そのタブリストの子ではない
page.getByRole('tablist', { name: 'セクション' }).getByRole('tab', { name: '請求' });

名前の付いたロールを持つ要素を見つけてから、その中を探してください。

const sections = page.locator('tanrei-tabs', { has: page.getByRole('tablist', { name: 'セクション' }) });
await sections.getByRole('tab', { name: '請求' }).click();

const confirm = page.locator('tanrei-dialog', { has: page.getByRole('dialog', { name: 'ファイルを削除しますか?' }) });
await confirm.getByRole('button', { name: '削除' }).click();

トップレイヤーに開くメニュー

開いているあいだ、<tanrei-menu> はアンカーの名前を持つメニューになりますが、ページの中では場所を取りません。面はトップレイヤーに描かれるからです。そのため、メニュー自体の表示を確かめると、開いていても失敗します。メニューがあることを確かめてから、その項目を確かめてください。

await page.getByRole('button', { name: 'その他' }).click();
const menu = page.getByRole('menu', { name: 'その他' });
await expect(menu).toBeAttached();
await expect(menu.getByRole('menuitem', { name: '名前を変更' })).toBeVisible();

閉じると要素はメニューではなくなり、await expect(menu).toHaveCount(0) が通ります。

警告

開発ビルドでは、仕様が認めない使い方をされた要素がコンソールにそのことを書きます。メッセージは [tanrei] で始まり、振る舞いの ID で終わります。ブラウザーのテストは、これが一つでも出たら失敗にできます。Playwright の例は AI エージェント向けのガイドにあります。