やってみた 2026年11月7日

Meta の Astryx を日本語で試したら、変換中の Enter は10部品とも無事で、テンプレート15本はビルドが通るのに白画面でした

XECIN TypeScriptデザインシステムReactアクセシビリティ

GitHubのトレンドに meta/astryx が載っていました。6位、スターは14,122で、その日だけで198増えています。説明は「An open source design system that’s fully customizable and agent ready」。README によると Meta の社内で8年育ったデザインシステムで、部品は150以上、ライセンスは MIT、まだベータ表記です。

気になったのは、README に約束が並んでいたことです。ビルドプラグインは要らない、部品はアクセシブル、人と AI エージェントが同じ作法で使える。受託の検収でライブラリを受け入れる立場だと、こういう言葉はそのまま信じず、確かめられる形に直したくなるんですよね。

テストは目的ではなく手段です。知りたかったのは「日本語の業務画面の土台として、どこまで任せてよいか」でした。そこで、触る前に受け入れ基準を決めました。

  • 変換を確定する Enter や、変換を取り消す Esc で、送信や選択や閉じる動作が走らない
  • 日本語の文言とカレンダーが出る
  • プラグインなしで動き、className で上書きできる
  • CLI が出すページテンプレートが、そのまま動く
  • テーマを変えても文字が読める

先に結果を書くと、1つ目はきれいに通りました。4つ目が想定外でした。

試した環境と、最初の数分

使い捨ての Docker コンテナ(node:22-bookworm、メモリ4GB・2CPU)の中で、Vite の React テンプレートに README の手順で入れました。Node.js 22.23.1、npm 10.9.8、React 19.3.1、Vite 8.3.6、TypeScript 7.0.2、Astryx は v0.7.10(コミット 2b827d6、2026-11-06 UTC 公開)です。ブラウザの検査はコンテナ内の Playwright 1.65.0 と axe-core 4.14.0、記事のキャプチャは Windows の Chrome 155 で撮りました。

# 雛形を作る
$ npm create -y vite@latest app -- --template react-ts --no-interactive   # 0.6秒
$ cd app && npm install --no-audit --no-fund                              # 9.9秒・28パッケージ
$ du -sh node_modules
96M	node_modules

# README のとおりに入れる
$ npm install --no-audit --no-fund @astryxdesign/core @astryxdesign/theme-neutral @stylexjs/stylex   # 3.3秒・16パッケージ
$ npm install --no-audit --no-fund -D @astryxdesign/cli                                              # 3.0秒・111パッケージ
$ du -sh node_modules
311M	node_modules

$ npx --no-install astryx component --list
Next step: run `npx astryx init` to finish setup and install the Astryx agent prompt.
Components (174)
(以下、部品の一覧)

--no-install を付けたのは、README に「scoped でない astryx は、CLI を依存に入れるまで無関係な npm パッケージに解決される」と注意書きがあったからです。部品数は174。AvatarGroupOverflow のような子部品も1件に数えた数ですが、「150以上」はそのとおりでした。

動かすまでは本当に早かったです。CSS を3行 import して Theme で包むだけで、Button が出ました。

Button 1個で CSS は 283 kB

「プラグイン不要」の代わりに何を払っているのかを、Vite のビルド結果で見ました。

構成JS(gzip)CSS(gzip)
React だけ(対照)219.47 kB(68.58)なし
Theme だけ(CSS は読まない)308.75 kB(96.27)なし
Button だけ(Theme も CSS も読まない)364.49 kB(115.48)なし
Button + Theme(@astryxdesign/core/Button から import)451.25 kB(142.19)283.64 kB(44.17)
Button + Theme(@astryxdesign/core から import)451.24 kB(142.11)283.64 kB(44.17)
Button + Theme(StyleX で部品もビルド。後述)454.49 kB(142.29)78.34 kB(13.86)

Button を使う構成には、このほかに Tooltip の別チャンク(約2 kB)が付きます。

分かったことは3つです。どこから import しても JS はほぼ同じで、tree-shaking は効いています。違いは変換モジュール数(2,215 と 3,062)とビルド時間(298ms と 470ms)だけでした。一方で CSS は、Button 1個でも全部品の分が載ります。そして StyleX のコンパイラを入れて部品ごとビルドすると(リポジトリの apps/example-vite の構成です)、CSS は 78.34 kB まで減り、ビルドは 2.33 秒に増えました。

ここは好みが分かれるところですが、社内の管理画面なら gzip で 44 kB を払って設定ゼロ、は悪くない取引だと思います。

変換中の Enter で、何も起きないか

日本語の業務画面でまず確かめたいのがここです。変換を確定した Enter でフォームが送信されたら、それだけで不具合票が立ちます。リポジトリ直下には IME_GUARD_DESIGN.md という設計メモがあり、TextInput の onEnter の説明にも「IME-safe」とあります。書いてあることは、測ります。

本物の IME はコンテナに入れていません。Chromium の DevTools プロトコルで変換中の状態を作り、キーイベントを送りました。

// 部品1つ x キー1つごとに、ページを開き直す(抜粋)
await cdp.send('Input.imeSetComposition', {text: 'にほんご', selectionStart: 4, selectionEnd: 4});
// 変換中に押されたキー。キー名はそのままで、keyCode は 229
await cdp.send('Input.dispatchKeyEvent', {type: 'rawKeyDown', key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 229, nativeVirtualKeyCode: 229});
await cdp.send('Input.insertText', {text: '日本語'});  // 確定
// 確定のあとに、同じ keydown をもう一度送る。最後に page.keyboard.press('Enter')

// ページ側で記録した keydown
// 変換中: key=Enter keyCode=229 isComposing=true
// 確定後: key=Enter keyCode=229 isComposing=false

正直に書くと、これはブラウザに届くイベントを再現しただけです。MS-IME や macOS の日本語入力の実機、Safari では確かめていないので、この検証にはまだ改善の余地があります。

大事なのは対照です。素の <input> に e.key === 'Enter' だけを見るハンドラを付けたものを同じ手順で叩き、この再現で事故が起きることを先に示しました。

部品キー変換中確定後の 229本物のキー
素の input(対照)Enter / Esc発火発火発火
TextInput(onEnter)Enter何も起きない何も起きない発火
ChatComposer(onSubmit)Enter何も起きない何も起きない発火
Typeahead / TokenizerEnter何も起きない何も起きない候補を選択
Selector / MultiSelector(hasSearch)Enter何も起きない何も起きない下矢印のあと Enter で選択
CommandPaletteEnter何も起きない何も起きない下矢印のあと Enter で選択
Typeahead / Tokenizer / Selector / MultiSelectorEsc閉じない未計測閉じる
CommandPalette / Dialog / Drawer / PopoverEsc閉じない閉じない閉じる

対照の素の input は3回とも発火しました。Astryx の10部品は、変換中と確定後の 229 では動かず(表の未計測を除く)、本物のキーでは動きます。「キーが死んでいるだけ」ではないことも、これで言えます。変換候補を選ぶつもりの下矢印でも aria-activedescendant は動かず、確定後の入力値はどの部品も 日本語 のままでした。

実装は utils/ime.js の isImeKeyEvent で、isComposing と keyCode === 229 の両方を見ています。設計メモが棚卸ししていた箇所は7つでしたが、0.7.10 の配布物では25ファイルがこの判定に触れていました。

正直なところ、ここが一番よい意味で予想を裏切られました。受け入れ基準の1つ目は、再現した範囲で合格です。

和暦は出る。日本語カタログは6割

InternationalizationProvider に ja-JP のカタログを渡し、Calendar に calendar="japanese" を付けて並べた画面です。カードの見出しと入力値は私が書いたものです。

Astryx の部品を日本語の設定で並べた画面。左上のカレンダーは令和8年11月と和暦で表示され、氏名の任意やファイルを選択も日本語。右上の複数選択は 1 selected と表示され、検索欄に総務と入れた行は Create 総務 と英語のまま

カレンダーの見出しは「令和8年11月」で、スクリーンリーダー向けのラベルも「令和8年11月7日土曜日、選択済み」でした。「任意」「ファイルを選択」「カレンダーを開く」「3 分前」も日本語です。ただし、右上の複数選択を開いて検索欄に「総務」と入れると、1 selected と Create "総務" は英語のままでした。

カタログを数えると、英語の 614 キーに対して ja-JP は 365 キーです。足りないのは comments が66、powersearch が35、richTextEditor が29、diffViewer が22、tokenizer が19 など。29 言語のどれも 365 キーだったので、日本語だけが遅れているわけではありません。文言の上書きは docs internationalization/i18n-library-integration に案内があるので、足りない分は自前で補う前提で見積もるのが安全です(上書きそのものは試していません)。

細かいところも3つ。ja-JP を渡しても <html lang> は Vite 雛形の en のままでした。neutral テーマの font-family は Figtree から始まって和文フォントの名前がなく、Web フォントも読み込まれない(document.fonts は0件)ので、日本語の字形は OS 任せです。CLI のドキュメントは --lang が英語、中国語、dense の3つで、astryx search "日付" は No results for "日付". でした。

className は効く。Tailwind は1行で結果が変わる

「No styling lock-in」も確かめました。素の CSS でも CSS Modules でも、className に渡した background-color と border-radius はそのまま勝ちます。Astryx のスタイルが @layer astryx-base の中にあり、レイヤーの外の CSS のほうが強いからです。さらに、hover の表現は background-image の薄いグラデーションだったので、背景色を変えても hover の反応は残りました。うまい作りだと思います。

Tailwind 4.3.5 は README に専用の手順があり、先頭に @layer の1行があります。この1行を省いて、ふつうに import するとどうなるかを比べました。

/* README の Tailwind 手順の先頭 */
@layer reset, theme, base, astryx-base, astryx-theme, components, utilities;

/* ブラウザで読み取ったレイヤーの順序(右ほど強い) */
/* README の順序         : reset, theme, base, astryx-base, astryx-theme, components, utilities */
/* Tailwind を先に import: theme, base, components, utilities, reset, astryx-base, astryx-theme */
/* Astryx を先に import  : reset, astryx-base, astryx-theme, theme, base, components, utilities */

Tailwind の読み込み順を変えた3段の図。上段の README の順序は、そのままのボタンが黒く、Tailwind 指定のボタンが赤い。中段の Tailwind を先に読む順序では、指定したボタンも黒いまま。下段の Astryx を先に読む順序では、そのままのボタンの背景と余白が消え、指定したボタンは赤いが余白がない

3つのビルドを撮って切り出し、縦に並べて左の説明を足した図です。右端の bridge は README の tailwind-theme.css を確かめるために置いた要素で、これを読んでいる上段だけ rounded-lg が 12px になります。

Tailwind を先に読むと utilities が Astryx より弱くなり、bg-red-500 は負けて背景は rgb(27, 27, 27) のままでした。Astryx を先に読むと今度は Tailwind の base が勝ち、何も指定していないボタンの背景は透明に、padding-left は 0px になります。どちらもビルドは通るので、画面を見るまで気づけません。README どおりなら問題ないので、これは不具合ではなく「その1行は飾りではない」という話です。

テンプレート15本が、ビルドは通るのに白画面

astryx init は 0.4 秒で AGENTS.md(2,093バイト)と GEMINI.md を書き出します。中身の方針は「start every page from a template, never from scratch」。人にも AI エージェントにも、まずテンプレートから作れと勧めています。ならばテンプレートが動くかどうかは、受け入れ基準に入れるべきです。

CLI のテンプレートはページ57本、ブロック710本ありました。ページ57本を全部書き出して tsc にかけると、recharts と @heroicons/react が無いという TS2307 が65件出ます。これは入れれば0件になりますし、パスを指定して書き出せば CLI が入れるべきものを教えてくれます。問題はブラウザで開いたあとでした。57本中15本が真っ白です。

$ npx --no-install astryx template shell-top-nav ./src/page.tsx
Copied template to src/page.tsx
(デモ画像の置き換えの案内は省略)
This template uses StyleX. Your project needs a StyleX compiler plugin - see `npx astryx docs styling-overview` for setup steps.

$ npx tsc -p tsconfig.app.json --noEmit                  # rc=0
$ npx vite build --outDir /tmp/crash-dist --emptyOutDir  # rc=0
$ npx --no-install astryx doctor --built .               # rc=0
pass: 10
warn: 2
fail: 0
info: 11
No failures - but review the [warn] warnings above when you can.

# ブラウザで開くと、#root の子要素は0個、body の文字数は0
Error: Unexpected 'stylex.create' call at runtime. Styles must be compiled by '@stylexjs/babel-plugin'.

型検査もビルドも診断コマンドも通って、画面だけが何も出ません。

「動いた=品質OK ではない」と普段から言っていますが、今回は「ビルドが通った=動いた」ですらありませんでした。

原因は単純で、この15本は @stylexjs/stylex を直接 import して、自分の中で stylex.create を呼んでいます。README の「No build plugins needed」は Astryx の部品の話で、テンプレートが自前で書いた StyleX は別なんですね。該当するのは dashboard- で始まる5本(comparison / scorecard / cohort-funnel / progress / alert-rail)、table- で始まる5本(collapsible / comparison / filter / inbox / tree)、それに canvas-editor、kanban-board、settings-dialog、shell-top-nav、work-item-detail です。dashboard、settings、login、table-page などの残り42本は、プラグインなしでも実行時エラーなしで開けました。

CLI も黙っているわけではありません。上のとおりパスを指定して書き出せば警告が出ますし、astryx swizzle Button にも同じ趣旨の警告があります(swizzle の実行時の失敗は試していません)。doctor の warn にも src/page.tsx:44 a stylex.create declaration と出ますが、これは「部品の prop やトークンを先に使おう」という書き方の助言でした。パスなしで標準出力に書き出す astryx template canvas-editor では、この警告は見当たりませんでした。

もう1つ気になったのは、AGENTS.md が最初に勧める astryx build です。astryx build "invoice list page" も astryx build "請求書の一覧ページ" も、出発点に shell-top-nav を挙げました。警告は添えられるものの、README の手順どおりの環境だと、勧められた1本目がそのまま白画面になります。

では、何を足せば動くのか。警告が案内する docs styling-overview の表から順に試しました。

構成vite build画面メモ
README のまま(コンパイラなし)rc=0真っ白CSS 283.64 kB
@stylexjs/rollup-plugin をそのまま(表が Vite 向けに案内)rc=1なしindex.css を Babel が読み、decorators の SyntaxError
同じプラグインに enforce: ‘post’(私の回避策)rc=0表示書き出された stylex.css(0.19 kB)は HTML から読まれない
@stylexjs/unplugin の stylex.vite()(astryx.css は残す)rc=0表示CSS 283.84 kB
@astryxdesign/build の astryxStylex()(リポジトリの apps/example-vite の構成)rc=015本すべて表示Button だけの例で CSS 78.34 kB

上の4行は shell-top-nav 1本で、5行目だけ15本全部で確かめています。ここでの「表示」は、実行時エラーがなく本文の文字が出たという意味で、見た目の差までは比べていません。Vite 8.3.6 で試した結果なので、Vite の版によって2行目の失敗は変わるかもしれません。

CSS が減る5行目には注意が1つあります。docs styling/tokens-and-setup には「astryx.css は読み続け、node_modules/@astryxdesign をコンパイル対象に入れない」とあり、リポジトリの例とは方針が違います。なので5行目は、案内された手順というより例に倣った実験です。私が手元に残すなら、astryx.css を残したまま足せる次の構成です。

// vite.config.ts
import {defineConfig} from 'vite';
import react from '@vitejs/plugin-react';
import stylex from '@stylexjs/unplugin';

export default defineConfig({plugins: [stylex.vite(), react()]});

axe で見えた範囲

「アクセシブル」も、機械で測れる範囲だけ測りました。ページテンプレート57本を neutral テーマのライトで開き、axe-core の WCAG 2.x A/AA のルールにかけています。StyleX の15本はコンパイラありの版で測りました(自作の index.html に title を書き忘れたせいで出た document-title は除いています)。

違反なしは45本、残り12本で合計51か所でした。多い順に、表示文字と aria-label がずれている label-content-name-mismatch が16か所、table-tree の aria-conditional-attr が13か所、dashboard-comparison の aria-hidden-focus が12か所、押す範囲が小さい target-size が8か所(table-page の絞り込みボタンは 12px 四方)、ほかに2か所です。

51か所のうち28か所は table-tree と dashboard-comparison の2本に集中しています。ただ、axe が見るのは機械で判定できる項目の、しかも初期表示だけです。違反0は合格の必要条件であって、十分条件ではありません。

テーマを変えると、読めなくなる組み合わせがある

7テーマのライトとダーク、代表の5本(login、settings、dashboard、table-page、form-wizard)で、コントラスト違反の数を合計しました。

テーマライトダークライトで違反した色の例
neutral / butter / stone / gothic00なし
chocolate1270#B88859 と #fffcf7 で 3.06:1
matcha1270#707e46 と #f0f0e0 で 3.83:1
y2k680#675d52 と #cccffa で 4.24:1

違反した文字色は、3テーマとも --color-text-secondary のライト側の値と一致します。小さめの補足文で使われていた色なので、このまま業務画面に載せるなら基準に照らして決めるところです。なお y2k の違反は body を塗ったときだけ出ます(塗らない白地では0件でした)。

実はこの表は測り直したものです。最初は何も塗らずに測り、ダークの全テーマで68〜176か所の違反が出て慌てました。ところが neutral のダークのスクリーンショットは、普通に読める暗い画面なんです。背景を何も塗っていないと axe は白として計算するらしく、body に --color-background-body を塗って測り直すとダークは全部0件になりました。今思えば、検査ツールの数字を画面と突き合わせなければ、誤った結論を書くところでした。

ただ、その調査で本物の問題も1つ見つかりました。gothic テーマです。

gothic テーマの設定画面を左右に並べた図。左は body を塗らない状態で、見出しやメニューの文字が白地に溶けてほとんど読めない。右は body に color-background-body を塗った状態で、暗い背景に文字が読める

同じ settings テンプレートを mode="light" で撮った2枚を左右に並べ、上の帯の文字を足した図です。gothic は mode="light" でも色が暗いままで、見出しの文字は #e8f1f6 です。Quick Start の手順には body を塗る指定が無く、ブラウザの地は白なので、コントラストは 1.14:1 になります。mode を省略して OS がライトの場合も、見出しの色は同じで body は透明でした。body に --color-background-body を塗れば右のように読めますが、この1行を知らないと左の画面になります。

CLI はオフラインでも動く

docker network disconnect で通信を切って CLI を叩くと、component、search、docs、template、doctor、build、manifest、discover はすべて rc=0 でした。止まったのは blog だけで、Could not reach https://astryx.atmeta.com/rss.xml: fetch failed です。ドキュメントが 23MB のパッケージに同梱されているので、閉じた環境で AI エージェントに使わせるのにも向きそうです。

一方で、トークン節約をうたう --dense の効果は小さめでした。component Button は 10,029 バイトが 8,666 バイトに、component Typeahead は 8,680 バイトのまま変わりません。

試してみての所感

最初に決めた受け入れ基準に戻ります。

受け入れ基準判定根拠
変換中の Enter / Esc で動作が走らない合格(再現した範囲)10部品で0件、対照は発火
日本語の文言とカレンダー条件付き和暦は出る。カタログは 614 キー中 365 キー
プラグインなしで動き、className で上書きできる合格部品について。Tailwind は README の順序が前提
CLI のページテンプレートがそのまま動く不合格57本中15本は StyleX コンパイラなしで白画面
テーマを変えても読める条件付き3テーマのライトで補足文字が 4.5:1 未満。gothic は body の塗りが必須

私なら、日本語入力の多い社内の管理画面の土台として候補に入れます。IME の扱いが部品をまたいで揃っているのは、検収する側から見るとかなり大きいです。

入れるなら最初に3つやります。@stylexjs/unplugin を入れておく。body を --color-background-body で塗る。そして、ページを開いて pageerror が0件で本文に文字があることを見るスモークテストを1本、CI に足す。今回の白画面は、型検査でもビルドでも診断コマンドでも止まらなかったので、ブラウザで開く検査でしか受け入れ基準を守れません。

様子見なのは更新の速さです。v0.7.0 が10月13日、v0.7.10 が11月6日なので、24日で11リリース出ています。まだベータ表記でもあり、版は固定して、astryx upgrade --apply は検証環境で試してから当てるつもりです(今回はここまで試していません)。Next.js での動作、実機の IME、Safari も未確認なので、次に触るならそこからですね。