ボタングループのスタイリングをJavaScriptからCSS継承に移行

shoelace-style/webawesome

wa-button-groupのグループスタイリングがJavaScriptによるクラス操作からCSSカスタムプロパティの継承に刷新されました。これにより、outlined ボタンの境界線が二重になるレースコンディション(#1975)が根本的に解消されます。

背景

従来の実装は、スロットに配置されたボタンを検出してJavaScriptでクラスを付与する方式でした。wa-button-groupはスロットの変更を検知するたびにupdateClassNames()を呼び出し、各ボタンにwa-button-group__button-firstwa-button-group__button-innerwa-button-group__button-lastといったクラスを付与していました。このDOMトラバーサルは、ボタンのappearanceプロパティを参照してhasOutlinedフラグを切り替える処理も含んでおり、コンポーネントの初期化タイミングによってはappearanceの値が未確定の状態でスタイルが計算されるレースコンディションを引き起こしていました。

その結果、outlined ボタンをwa-button-group内に並べると、隣接するボタン間の境界線が二重に表示されるという不具合(#1975)が発生していました。JavaScriptがappearance === 'outlined'を検知してgap: 0に切り替えるまでの間、デフォルトのgap: 1pxでレンダリングされるためです。CSSによる純粋な宣言的スタイリングへの移行は、この問題を構造的に解消するアプローチです。

技術的な変更

スタイリングの制御はCSSカスタムプロパティの継承に一本化され、JavaScriptのDOM操作は削除されました。

button-group.tsからupdateClassNames()メソッドとhandleSlotChange()メソッドが完全に削除されました。classMapディレクティブも不要になり、lit/directives/class-map.jsのインポートも除去されています。orientation変更時にupdateClassNames()を呼び出していたupdated()フックの該当行も削除されました。

代わりにbutton-group.styles.tsで、wa-button-groupホスト要素が内部カスタムプロパティ--_wa-button-*プレフィックス)をCSSの継承を通じてスロット内のボタンへ伝達するようになりました:

/* 水平方向のグループ設定 */
:host([orientation='horizontal']) {
  --_wa-button-horizontal-indent: 1px;
  --_wa-button-horizontal-indent-outlined: calc(var(--wa-border-width-s) * -1);
}

/* 垂直方向のグループ設定 */
:host([orientation='vertical']) {
  --_wa-button-vertical-indent: 1px;
  --_wa-button-vertical-indent-outlined: calc(var(--wa-border-width-s) * -1);
}

/* 中間のボタンはすべての角丸を除去 */
::slotted(:not(:first-child):not(:last-child)) {
  --_wa-button-start-start-radius: 0;
  --_wa-button-start-end-radius: 0;
  --_wa-button-end-start-radius: 0;
  --_wa-button-end-end-radius: 0;
}

/* 先頭ボタン(水平)は末尾側の角丸のみ除去 */
:host([orientation='horizontal']) ::slotted(:first-child) {
  --_wa-button-start-end-radius: 0;
  --_wa-button-end-end-radius: 0;
}

button.styles.tsでは、従来の単一のborder-radius指定が4つの論理プロパティborder-start-start-radiusなど)に分割され、各プロパティがデフォルト値として--wa-form-control-border-radiusを持つカスタムプロパティで上書き可能になりました。また、間隔制御はグループ側のgapではなくボタン側のmargin-inline-startmargin-block-startとして実装され、appearanceごとに異なるプロパティを参照します:

:host([appearance='outlined']) {
  margin-inline-start: var(--_wa-button-horizontal-indent-outlined);
  margin-block-start: var(--_wa-button-vertical-indent-outlined);
}

:host([appearance='filled']) {
  margin-inline-start: var(--_wa-button-horizontal-indent);
  margin-block-start: var(--_wa-button-vertical-indent);
}

outlined ボタンでは--_wa-button-horizontal-indent-outlinedcalc(var(--wa-border-width-s) * -1)(負のマージン)に設定されるため、隣接する境界線を重ねて一本に見せます。JavaScriptでhasOutlinedフラグを検出してgapを切り替える処理がなくなり、初期レンダリングから正しくスタイルが適用されます。

テストもクラスの存在確認からCSSカスタムプロパティの継承確認に書き換えられ、各ボタンの位置に応じた--_wa-button-*-radiusプロパティの値を直接アサートする形になっています。

設計判断

--_wa-button-*という命名規則によって内部プロパティとパブリックAPIの境界を明確化したことが、この変更の重要な設計判断です。アンダースコアプレフィックスはJavaScriptの慣習から着想を得たもので、PRではトリプルダッシュ(---wa-*)や--$wa-*なども検討されましたが、変更容易性を考慮してアンダースコアが採用されています。

もう一つの注目すべき判断は「角丸の除去方針」です。グループがボタンの角丸をすべて一旦ゼロにしてから必要な分だけ戻す方式ではなく、「不要な角丸のみを除去する」方式が採用されました。ボタン側が自身のサイズやpill設定に応じた角丸の値を保持し、グループは「この角は不要」という情報だけを0で上書きする設計です。これにより、グループがボタンの内部的な角丸計算ロジックを知る必要がなくなっています。

間隔制御をグループのgapからボタンのmarginに移したことで、outlined とそれ以外のボタンを混在させたグループもappearance属性ベースのCSSセレクタで自動的に正しい間隔が適用されます。JavaScriptでボタンのappearanceを読んで判断する必要がなくなった結果として、レースコンディションが消滅しています。

まとめ

この変更は、JavaScriptによる命令的なDOM操作を宣言的なCSSカスタムプロパティの継承に置き換えることで、レースコンディション起因のスタイル崩れを構造的に排除しています。--_wa-button-*という内部プロパティの規約によって親子コンポーネント間の暗黙的な結合を明文化しつつ、ボタン側がグループのロジックを知らなくて済む責務分離を実現した設計といえます。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
bb12dff5

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)→背景・技術変更・設計判断(各論)→まとめ(結論)という3部構成が明確に適用されており、ガイドラインに準拠しています。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```css:path```)およびGitHubのPR・Issueリンク記法([#123](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

CSSカスタムプロパティの継承やレースコンディションといったトピックを扱っており、専門知識を持つエンジニアという対象読者に適合しています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論で構成され、各段落はトピックセンテンスで始まり、1段落1トピックの原則が守られています。段落の長さも適切で可読性が高いです。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内で引用されているCSSコードは、提供されたDiffの内容とファイル名を含めて正確に一致しています。JSファイルの削除に関する説明もDiffと整合しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「レースコンディション」「CSSカスタムプロパティ」「論理プロパティ」「責務分離」などの技術用語が、文脈に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

JavaScriptによるクラス付与が引き起こすレースコンディションのメカニズムと、CSSカスタムプロパティの継承と負マージンによる解決策の説明が、技術的に正確かつ論理的です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張(レースコンディションの修正、内部プロパティの命名規則、角丸の除去方針など)は、PRのDescriptionやDiffの内容によって裏付けられており、ハルシネーションは検出されませんでした。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#2044)およびIssue番号(#1975)が正確に記載・リンクされています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトル「ボタングループのスタイリングをJavaScriptからCSS継承に移行」は、PRの主題である「pure CSSへのリファクタリング」を的確に表現しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事には、バージョンサポート状況やリリース日程など、PR情報に基づかない外部知識は含まれていません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「従来の実装」「刷新されました」といった時間表現は、PRの文脈と一致しており、事実を正確に伝えています。