styling について
stylingは、SvelteプロジェクトでTailwindや共有コンポーネントの規約に沿ったUIスタイルを実装するスキルです。フレックスレイアウト内のスクロール領域のサイズ調整方法も説明します。
主な機能
- 01不要なラッパー要素の削減
- 02Tailwindユーティリティとセマンティックトークンの活用
- 03tailwind-variantsによるコンポーネントバリアント管理
- 04操作不可状態にHTMLのdisabledを使用
- 05フレックス列内のスクロール領域を調整
活用シーン
- 01Svelte UIのスタイル記述を統一
- 02共有コンポーネントに適切な上書きを選択
- 03パネル内コンテンツが個別にスクロールしない問題の修正
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ epicenterhq/ epicenter --skill stylingインストール後、Claude Code で/stylingを入力して読み込まれたか確認します。
詳細
スタイリングガイドライン
スタイリングが shadcn-svelte の構造、クラスのマージ、バリアント、Bits UI のコンポジション、またはローカルラッパーの動作に依存する場合は、上流の情報を確認するため、ui-design のコンポーネントシステムリファレンスを参照してください。通常の Tailwind ユーティリティと、以下のリポジトリ固有のレイアウトルールについては、外部情報を確認する必要はありません。
ラッパー要素を最小限にする
不要なラッパーの div は作成しないでください。同じ結果を既存のセマンティック要素にクラスを直接適用して実現できる場合は、そちらを優先してください。
良い例(直接適用)
<main class="flex-1 mx-auto max-w-7xl">
{@render children()}
</main>
避ける例(不要なラッパー)
<main class="flex-1">
<div class="mx-auto max-w-7xl">
{@render children()}
</div>
</main>
この原則は、スタイリングが要素のセマンティックな目的と矛盾したり、レイアウト上の問題を引き起こしたりしない限り、すべての要素に当てはまります。
Tailwind のベストプラクティス
- 条件に応じてクラスを結合するには、アプリでは
@epicenter/ui/utilsのcn()を、UI パッケージ内では../utils.jsのcn()を使用します - ローカルのレイアウトや状態には、カスタム CSS よりユーティリティクラスを優先します
- 任意の角括弧付きの値や生の色より、共有スケールとセマンティックトークンのユーティリティを優先します
- コンポーネントのバリアントシステムには
tailwind-variantsを使用します - 色には
background/foregroundの規則に従います - テーマの一貫性を保つために CSS 変数を活用します
共有プリミティブのオーバーライド
ローカルの @epicenter/ui プリミティブをスタイリングする場合は、ui-design のコンポーネントシステムリファレンスを使用してください。共有プリミティブに適用する Tailwind クラスでは、通常、プリミティブの見た目の仕様を再定義するのではなく、親のレイアウトや製品の状態を表現します。
良いプリミティブのオーバーライド:
<Item.Button size="sm" class="w-full justify-start text-left" />
疑わしいプリミティブのオーバーライド:
<Item.Button size="sm" class="gap-2 rounded px-2 py-1.5 text-sm hover:bg-accent/50" />
オーバーライドでサイズ、密度、角丸、間隔、パディング、タイポグラフィ、ホバー、フォーカス、トランジションのクラスを繰り返している場合は、プリミティブにサイズ、バリアント、またはセマンティックラッパーが必要ではないか検討してください。
無効状態:HTML の disabled と Tailwind のバリアントを使う
空のセクション、読み込み中、アイテムがない場合など、インタラクティブな要素を操作できない状態にする場合は、JS の条件ガードではなく HTML の disabled 属性を使用してください。Tailwind の enabled: と group-disabled: のバリアントを組み合わせてください。
JS ガードより disabled を使う理由
disabledはクリックをネイティブにブロックするため、if (!hasItems) returnは不要です- スタイリングに使用できる
:disabledCSS 疑似クラスが有効になります - アクセシビリティの観点で意味的に適切です(スクリーンリーダーは「無効」または「利用不可」と読み上げます)
- Tailwind の
enabled:とgroup-disabled:のバリアントはきれいに組み合わせられます
パターン
<button
class="group enabled:cursor-pointer enabled:hover:opacity-80"
disabled={item.count === 0}
onclick={toggle}
>
{item.label} ({item.count})
<ChevronIcon class="group-disabled:invisible" />
</button>
主なバリアント
enabled:cursor-pointer:クリック可能な場合にのみポインターカーソルを表示しますenabled:hover:bg-accent/50:操作可能な場合にのみホバー効果を適用しますgroup-disabled:invisible:親が無効な場合に子要素(展開用シェブロンなど)を非表示にしますdisabled:opacity-50:無効な要素を薄く表示します
アンチパターン
<button
class="cursor-pointer hover:opacity-80"
onclick={() => { if (item.count > 0) toggle(); }}
>
JS ガードでは、操作できない要素でも cursor-pointer と hover:opacity-80 が有効なままです。ユーザーには、クリックしても何も起こらないボタンが表示されます。disabled を使い、ブラウザーと CSS に処理を任せてください。
Flex カラムのスクロールトラップ
Flex の子要素が h-full(高さ:100%)を使用し、兄弟要素(ヘッダー、ツールバー、フッター)と同じ flex カラム内にある場合、親要素の全体の高さで計算されます。その結果、残りのスペースを使うのではなく、兄弟要素を越えてはみ出します。コンテンツが切り取られたり、レイアウトがビューポートからはみ出したりして、内部のスクロール領域が機能しなくなります。
これは、このコードベースで最もよく発生するレイアウトバグです。次のような場合に発生します。
- スクロールが必要な
Resizable.Pane(paneforge)内のコンポーネント - ヘッダーやツールバーが兄弟要素としてある flex カラム内の
ScrollArea.Root(bits-ui)またはoverflow-autoの div - セクションごとに独立してスクロールする必要がある分割ペインやパネルのレイアウト
修正方法:flex-1 min-h-0 overflow-hidden
スクロール可能なコンテンツを含むフレックス子要素で、h-full をこの3つのユーティリティに置き換えます。それぞれ異なる問題を解決します。
| ユーティリティ | 機能 | 必要な理由 |
|---|---|---|
flex-1 | 兄弟要素の後に残ったスペースを使います | h-full は兄弟要素を考慮せず、親の100%の高さになります。flex-1 は残りのスペースを使います。 |
min-h-0 | コンテンツのサイズより小さく縮小できるようにします | フレックスアイテムのデフォルトは min-height: auto であり、コンテンツより小さくなるのを防ぎます。 |
overflow-hidden | 高さを制限するコンテキストを確立します | これがないと、overflow-auto や ScrollArea を持つ子要素にはスクロールの基準となる高さの上限がありません。 |
3つすべてが必要です。どれか1つでも欠けると、修正は機能しません。
flex-1がない場合:要素は親の100%のままなので、兄弟要素にはみ出しますmin-h-0がない場合:要素は縮小できず、コンテンツによって高さが増えますoverflow-hiddenがない場合:内側のスクロールコンテナには高さが制限された祖先要素がないため、スクロールせずに拡張します
修正前/修正後
<main class="flex h-full flex-col overflow-hidden">
<div class="border-b px-4 py-2">Toolbar</div>
<MyScrollableContent class="h-full" />
</main>
<main class="flex h-full flex-col overflow-hidden">
<div class="border-b px-4 py-2">Toolbar</div>
<MyScrollableContent class="flex-1 min-h-0 overflow-hidden" />
</main>
リサイズ可能なペイン内(paneforge)
Paneforge の Pane コンポーネントはフレックス比で幅を設定しますが、高さを制限したり、オーバーフローをクリップしたりはしません。Pane 内のスクロール可能なコンテンツでは、ルート要素に flex-1 min-h-0 overflow-hidden のチェーン全体を指定する必要があります。
<Resizable.Pane defaultSize={80}>
<div class="flex flex-1 min-h-0 flex-col overflow-hidden">
<div class="border-b">Header</div>
<div class="flex-1 overflow-y-auto">
</div>
</div>
</Resizable.Pane>
ScrollArea を使用する場合(bits-ui)
ScrollArea.Root は position: relative を指定してレンダリングされ、ビューポートには height: 100% が指定されます。これによりフレックスのサイズ計算の連鎖が崩れます。ビューポートのパーセント指定の高さは、フレックスコンテキストで明示的な高さを持たない relative の親要素を基準に計算されるためです。その結果、コンテンツはスクロールせずに拡張します。
選択肢は2つあります。
flex-1 min-h-0を指定した div で、シンプルで常に機能するoverflow-y-autoを使用する- スタイル付きスクロールバーが必要な場合:
ScrollArea.Rootをflex-1 min-h-0 overflow-hiddenを指定した div で囲み、高さが制限された祖先要素を用意する
<div class="flex-1 overflow-y-auto">
{#each items as item}
<div>{item.name}</div>
{/each}
</div>
<div class="flex-1 min-h-0 overflow-hidden">
<ScrollArea.Root class="h-full">
{#each items as item}
<div>{item.name}</div>
{/each}
</ScrollArea.Root>
</div>
経験則
同じフレックス列に兄弟要素があるフレックス子要素に h-full を指定しようとしたら、いったん止めて flex-1 min-h-0 overflow-hidden に置き換えてください。h-full のパターンが機能するのは、その要素がフレックス親の唯一の子要素である場合だけです。