frontend-style-guide について
Mantine 8 を使って Lightdash のフロントエンドコンポーネントを開発するエージェント向けのスタイルガイドです。テーマトークン、CSS Modules、共有コンポーネント、ライト・ダークモードの規則を示します。
主な機能
- 01Mantine 8 のテーマとコンポーネント規則
- 02色・文字・余白のトークン
- 03CSS Modules の使用指針
- 04ライト・ダークモードの確認
- 05コンポーネント状態とアクセシビリティ
活用シーン
- 01フロントエンドコンポーネントの作成・更新
- 02テーマトークンに沿った UI スタイルの統一
- 03配色とボタン階層の確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ lightdash/ lightdash --skill frontend-style-guideインストール後、Claude Code で/frontend-style-guideを入力して読み込まれたか確認します。
詳細
翻訳後に原文が更新されています。最新の内容は原文をご確認ください。
Lightdash フロントエンドスタイルガイド
packages/frontend/ のフロントエンドコンポーネントを扱う際は、これらのルールに従ってください。
Mantine 8
アプリは Mantine 8(@mantine/core)を使用し、テーマは src/theme/ にあります。
デザイン原則
テーマは、shadcn、Radix、Kumoを参考にした、ニュートラルで控えめなデザインです。アクセントカラーは1色、フラットなサーフェス、柔らかな境界線、コンパクトな文字サイズのスケールで構成されています。「見た目がしっくりくる」状態は、ほとんどの場合、デフォルトを使うことで実現します。色、シャドウ、サイズを追加する前に、それぞれのルールを確認してください。
- アクセントは1色で、インクカラーです。 サーフェス上の主要なアクションにはテーマのプライマリカラー(ライトモードではほぼ黒、ダークモードではほぼ白)を使います。強調のために Button や ActionIcon を
blue、dark、indigo、ldDarkに色付けしないでください。コントロールの色は状態を表します。破壊的な操作はred、警告はyellow、「コピー済み」はteal、お気に入りはorange、検証済み状態(チェックアイコン、バッジ)と既存のマージアクションはgreenです。検証ボタンの塗りつぶしにgreenは使いません。インディゴはAI関連のサーフェスに使います。青いテキストを使うのはリンクだけです。 - 階層は色ではなくバリアントで示します。
filledは、カード、ヘッダー、モーダルのフッターごとに1つだけ使うプライマリアクションです。default(枠線付き)はセカンダリです。lightはターシャリ、またはトグルのようなアクション(「フィルターを追加」)に使います。subtleはアイコンボタンやインラインアクション用で、ActionIcon のデフォルトです。1つのサーフェスにfilledボタンが2つある場合、どちらかが誤りです。 - サーフェスはフラットにします。 Paper と Card はデフォルトで枠線付き、シャドウなし、角丸は12pxです。シャドウを使うのは、浮かせるレイヤー(Menu と Popover は
md、Modal はlg)だけです。これらを再指定するためにwithBorder、shadow、radiusを渡さないでください。空のセクションやプレースホルダーのセクションには<Paper variant="dotted">を使います。 - ニュートラルカラーはトークンを使い、手動で組み合わせません。
--mantine-color-body(サーフェス)、--ld-color-page(キャンバス)、default-border、default-hover、text、dimmed、placeholderを使います。ldGray.Nはカラースキームに応じてすでに解決されます(0 はキャンバス、1 はミュートされた塗りつぶし、2 は境界線、3 は強い境界線、5 はターシャリテキスト、6 は dimmed、7 はラベル、9 はテキスト)。そのため、ニュートラルカラーにlight-dark(ldGray-x, ldDark-y)や@mixin darkブロックを使うのは、いずれも避けるべき兆候です。セカンダリテキストにはc="dimmed"を使います。 - 文字サイズはスケールに従い、スライダーのように調整しません。 本文は14/20です。見出しは
Titleの order 1〜6で600(28〜14px)、ラベルとテーブルヘッダーは500、それ以外は400です。fz="xs|sm|md"を使い、fz={13}は使いません。カードとセクションのタイトルにはTitle order={5}を使い、ページ上部にはPageHeaderを使います。 - スペースにはトークングリッドを使います。 カードのパディングは
md、グループ間のギャップはxs/sm、セクション間のギャップはlg、ページのガターはlg/xlです。ピクセル単位のマージンは使いません。レイアウトにmargin-top: 20pxが必要なら、mt="lg"を使います。 - アイコンは控えめにします。 テキストの横にはストローク幅1.5、16pxの
MantineIconを使い、xsコントロールでは14pxにします。セカンダリのアイコンにはdimmedを指定します。アイコンのみのボタンには、必ずaria-labelと Tooltip を付けます。 - 入力欄は落ち着いた見た目にします。 デフォルトのバリアントを使い、境界線は柔らかく、フォーカス時はリングを付けずに境界線を濃くします。
size="xs"のコントロールには、コンパクトなセカンダリラベルが自動的に表示されます。Select では選択済みの項目にチェックマークを付けます。フィルター値の選択には、カスタムリストではなく標準のコンボボックスを使います。 - モーションは機能のために使います。 ドロップダウンはアンカーから展開し、モーダルはフェードします。どちらもテーマの設定に従います。進行状況を示す場合を除き、それ以外はアニメーションさせません。
- ダークモードは導出するのではなく、設計します。 完了前に両方のカラースキームを確認してください。色を選ぶために
useMantineColorSchemeを読み取らず、トークンを使います。エディターではuseEditorTheme()を使います。CSSを使わないJSの利用箇所(ECharts、Leaflet)だけ、useComputedColorSchemeを読み取ります。 - 状態表示には共通コンポーネントを使います。 読み込みには
EmptyStateLoader、セクションの読み込み失敗にはInlineErrorState、ページの読み込み失敗にはSuboptimalState、「ここには何もありません」の表示には<Paper variant="dotted">を使います。 - CSSを書く前に、すでにあるものを探します。 バリアント、トークン、
ld-*ユーティリティクラス、または共有コントロール(CopyActionIcon、FavoriteActionIcon、ConfirmDeleteButton、TruncatedText、FilterFacet、NumberInput、MantineModal)を確認してください。 画面を引き渡す前のセルフレビュー: ライトモードとダークモードの両方で開いて確認します。サーフェスごとに塗りつぶしボタンの数を数えます(最大1つ)。リンクではない青がないか探します。カード上のシャドウがないか探します。トークンではないフォントサイズやグレーがないか探します。ツールチップのないアイコンボタンがないか探します。迷ったときは、Explorer ページが参照すべきサーフェスです。
コンポーネントチェックリスト
コンポーネントを作成・更新する際は:
-
@mantine/coreのインポートを使用する -
styleまたはstylesプロップを使用しない - 利用可能なコンポーネントプロップについて Mantine のドキュメント/型を確認する
- 利用可能な場合はスタイリングにインラインスタイルのコンポーネントプロップを使用する(かつ3プロップ以下のルールに従う)
- コンポーネントプロップが利用できない場合、または3つを超えるインラインスタイルプロップが必要な場合は CSS モジュールを使用する
- エラーメッセージや、埋め込みから到達可能なコンポーネントのラベルに「このアプリ」を意味する「Lightdash」を含めない(
docs/white-label-copy.mdを参照) - 埋め込みに Lightdash のブランディングやドキュメントリンクを含めない:
useIsEmbedded()でゲートする(packages/frontend/CLAUDE.mdの「Embeds are white-label」を参照) - マジックナンバーの代わりにテーマ値('md'、'lg'、'xl'、または 'ldGray.1'、'ldGray.2'、'ldDark.1'、'ldDark.2' など)を使用する
- CSS モジュールで Mantine のカラーを使用する場合は、常にテーマ対応の変数を使用する:
--mantine-color-${color}-text: 塗りつぶし背景上のテキスト用--mantine-color-${color}-filled: 塗りつぶし背景用(強い色)--mantine-color-${color}-filled-hover: ホバー時の塗りつぶし背景用--mantine-color-${color}-light: 淡い背景用--mantine-color-${color}-light-hover: ホバー時の淡い背景用(淡い色)--mantine-color-${color}-light-color: 淡い背景上のテキスト用--mantine-color-${color}-outline: アウトライン用--mantine-color-${color}-outline-hover: ホバー時のアウトライン用
スタイリングのベストプラクティス
基本原則: テーマファースト
目標は、可能な限りテーマのデフォルトを使用することです。 スタイルの上書きは例外であるべきで、原則ではありません。
スタイリングの階層
- 最善: カスタムスタイルなし(テーマのデフォルトとバリアントを使用)
- テーマ拡張: 繰り返しパターンの場合は、
src/theme/components/<Component>.module.cssにバリアントまたはルールを追加する(src/theme/components/index.tsに登録) - コンポーネントプロップ: 単純な上書き(
mt="xl" w={240}のような1〜3個のプロップ) - CSS モジュール: 複雑なスタイリング、3個を超えるプロップ、または Mantine にプロップが存在しない単一のルール(
align-self、overflow、cursor、white-space)を、ファイルのモジュール内で役割に基づいて命名したクラスとして記述する。flex-shrink/flex-growはflexスタイルプロップです。グローバルユーティリティクラスは使用しないでください。
決して使用しないもの
stylesプロップ(常に CSS モジュールを代わりに使用)styleプロップ(インラインスタイル)
テーマ拡張(繰り返しパターン用)
同じスタイル上書きを何度も適用していることに気づいたら、それをテーマに配置します。各コンポーネントには src/theme/components/ に CSS モジュールがあり、src/theme/components/index.ts にエントリがあります:
/* src/theme/components/Badge.module.css */
.root[data-variant='light'] {
text-transform: none;
font-weight: 500;
}
// src/theme/components/index.ts
Badge: Badge.extend({
defaultProps: { variant: 'light', color: 'gray' },
classNames: badgeClasses,
}),
vars コールバックに頼るのは、Mantine が値をインラインで書き込む場合(ボタンとバッジの色、NavLink の塗りつぶし、入力のフォントサイズ)だけにしてください。CSS はインラインのカスタムプロパティを上書きできないからです。
コンテキスト固有の上書き
インラインスタイルのコンポーネントプロップ(1〜3個の単純なプロップ)
// ✅ Good
<Button mt="xl" w={240} c="blue.6">Submit</Button>
// ❌ Bad - Too many props, use CSS modules instead
<Button mt={20} mb={20} ml={10} mr={10} w={240} c="blue.6" bg="white">Submit</Button>
よく使うインラインスタイルプロップ:
- レイアウト:
mt、mb、ml、mr、m、p、pt、pb、pl、pr - サイズ:
w、h、maw、mah、miw、mih - 色:
c(色)、bg(背景) - フォント:
ff、fs、fw - テキスト:
ta、lh
CSS モジュール(複雑なスタイル、または3個を超えるプロップ)
コンポーネントと同じフォルダに .module.css ファイルを作成します:
/* Component.module.css */
.customCard {
transition: transform 0.2s ease;
cursor: pointer;
}
.customCard:hover {
transform: translateY(-2px);
box-shadow: var(--mantine-shadow-lg);
}
import styles from './Component.module.css';
<Card className={styles.customCard}>{/* content */}</Card>;
.css.d.ts ファイルを含めないでください - Vite がこれを自動的に処理します。
カラーガイドライン
コンポーネントのデフォルトカラーを優先してください。 ボタン、ActionIcon、バッジはテーマから適切なニュートラルまたはインクを取得します。コントロールの color プロップは、状態を表す場合にのみ使用してください(デザイン原則を参照)。
// ❌ Bad - restates the theme, and is a near-black button in dark mode
<Button color="dark">Apply</Button>
<ActionIcon color="ldGray.6" variant="subtle" />
// ✅ Good - the theme already renders these
<Button>Apply</Button>
<ActionIcon />
// ❌ Bad - hand-picked grey
<Text c="ldGray.6">Secondary text</Text>
// ✅ Good - semantic token
<Text c="dimmed">Secondary text</Text>
ニュートラルトークン
| トークン | 用途 |
|---|---|
--mantine-color-body | サーフェス(カード、入力、メニュー) |
--ld-color-page | サーフェスの背後にあるページキャンバス |
--mantine-color-default-border / default-hover | ニュートラルなコントロールのボーダーとホバー時の塗り |
--mantine-color-text / dimmed / placeholder | プライマリ、セカンダリ、ターシャリのテキスト |
ldGray.0-9 | 両スキームで同じ役割: 0 はキャンバス、1 は控えめな塗り、2 はボーダー、3 は強いボーダー、4 は淡いアイコン、5 はターシャリテキスト、6 は dimmed、7 はラベル、9 はテキスト |
CSS Modules でのダークモード
ニュートラルにはダークモード用の分岐は不要です。トークンと ldGray.N はすでにスキームごとに解決されます。
/* ❌ Bad */
.row:hover {
background-color: var(--mantine-color-ldGray-0);
@mixin dark {
background-color: var(--mantine-color-ldDark-5);
}
}
/* ✅ Good */
.row:hover {
background-color: var(--mantine-color-default-hover);
}
light-dark() は、トークンを持たない非ニュートラルなペア(アクセントの色合いなど)にのみ使用してください:
.highlight {
background-color: light-dark(
var(--mantine-color-indigo-0),
var(--mantine-color-indigo-9)
);
}
常にテーマトークンを使用する
// ❌ Bad - Magic numbers
<Box p={16} mt={24}>
// ✅ Good - Theme tokens
<Box p="md" mt="lg">
不要なスタイルを削除する
スタイルを CSS modules に移す前に、それらが実際に必要かどうかを確認してください:
// ❌ Unnecessary - display: block has no effect on flex children
<Flex justify="flex-end">
<Button style={{display: 'block'}}>Submit</Button>
</Flex>
// ✅ Better - Remove the style entirely
<Flex justify="flex-end">
<Button>Submit</Button>
</Flex>
共有レイアウト用 CSS 変数(高さ、幅、z-index)
横断的なレイアウト定数(ナビゲーションバー/ヘッダー/バナー/フッターの高さ、ページコンテンツの幅、サイドバーの寸法、ダッシュボードのヘッダー/タブの高さと z-index)はグローバル CSS 変数として公開されているため、CSS modules から直接使用できます:
/* ✅ Reference the global var — resolves on :root everywhere */
.myPanel {
top: var(--dashboard-header-height);
max-width: var(--page-content-max-width-large);
}
/* ❌ Don't hardcode the literal — drifts from the source of truth */
.myPanel {
top: 50px;
}
// ❌ Don't bridge a constant into CSS via an inline style object
<div style={{ '--dashboard-header-height': `${DASHBOARD_HEADER_HEIGHT}px` }}>
信頼できる情報源: 数値はそれぞれの */constants.ts ファイル(例: components/common/Page/constants.ts、components/common/Dashboard/dashboard.constants.ts)にあり、src/theme/cssVariablesResolver.ts で CSS 変数として登録されています(Mantine の cssVariablesResolver を介して Mantine プロバイダーに接続されています)。独自の変数を定義する前に、利用可能な var(--...) 名の完全なリストについてはこのファイルを参照してください。
新しい共有レイアウト定数を追加するには: 該当する constants.ts に数値を追加し、src/theme/cssVariablesResolver.ts に登録してから、CSS で var(--your-name) を参照します。リテラル値を .module.css ファイルで再宣言したり、インラインの style で渡したりしないでください。JS の値として必要な場合(Mantine の h= プロパティなど)は、TS 内で数値定数を直接使い続けてください。
テーマを認識するコンポーネントロジック
色を選ぶためにカラースキームを読み取らないでください。トークンを渡し、CSS に解決させてください。正当な読み取り元は、プレーンな JS の値を使用するコードエディターとチャートライブラリの 2 つだけです:
// Monaco / Ace theme names
const { monaco, ace } = useEditorTheme();
// ECharts, Leaflet and other non-CSS consumers
const isDark = useComputedColorScheme('light') === 'dark';
@mantine/core がエクスポートする clsx ユーティリティを使用する
import { clsx } from '@mantine/core';
const MyComponent = () => {
return (
<div className={clsx('my-class', 'my-other-class')}>My Component</div>
);
};
Select/MultiSelect のグループ化
<Select
label="Your favorite library"
placeholder="Pick value"
data={[
{ group: 'Frontend', items: ['React', 'Angular'] },
{ group: 'Backend', items: ['Express', 'Django'] },
]}
/>
再利用可能なコンポーネント
モーダル
- 常に
components/common/MantineModalのMantineModalを使用してください - Mantine の Modal を直接使用しないでください - 使用例については
stories/Modal.stories.tsxを参照してください - モーダル内のフォームの場合: フォームに
idを、送信ボタンにform="form-id"を使用してください - モーダル内のアラートの場合:
Calloutをdanger、warning、infoのバリアントで使用してください
共有コントロール
- クリップボードにコピー:
components/common/CopyActionIconのCopyActionIcon(value、オプションのcopyLabel/copiedLabel/icon/tooltipPosition、さらに ActionIcon のプロパティ)。CopyButton+ActionIcon+ アイコンの切り替えを自作しないでください。 - お気に入り切り替え:
components/common/FavoriteActionIconのFavoriteActionIcon(isFavorite、onToggle、オプションのラベル用のname)。 - 2 クリック削除:
components/common/ConfirmDeleteButtonのConfirmDeleteButton(onConfirm、aria-label、オプションのtooltip)。最初のクリックで待機状態になり、2 回目で実行され、フォーカスが外れるかタイムアウトすると解除されます。 - ファセットフィルター(検索、件数、グループ、すべて選択を備えたポップオーバー):
components/common/FilterFacetのFilterFacet。
コールアウト
components/common/CalloutのCalloutを使用してください- バリアント:
danger、warning、info
空/利用不可のセクション(点線スタイル)
<Paper variant="dotted">(Cardも同様)は、透明な背景に破線の枠線を描画します。これは、空、プレースホルダー、または利用不可のセクションで使う標準スタイルです。src/theme/components/Paper.module.cssで定義されており、たとえばFavoritesPanelやAiAgentKnowledgeFilesSectionで使用されています。- セクションの読み込みに失敗した場合:
components/common/InlineErrorStateのInlineErrorStateを使用します。控えめなメッセージと任意のonRetryボタンを備えた、点線スタイルのPaperです。目立たせないようにしてください。副次的なパネルの読み込み失敗で大きく警告する必要はありません。 - プレースホルダー値(データのない統計タイルなど):見せかけのゼロや終わりのないスケルトンではなく、点線のコンテナー内に
ldGray.5でエムダッシュ(—)を表示します。スケルトンは「読み込み中」、点線は「ここには何もありません」を意味します。 - ページ全体の読み込み失敗には、
ErrorState/SuboptimalStateを使用します。
多態的なクリック可能コンテナー
レイアウト用コンテナーをクリック可能にする必要がある場合は、これらを使用してください。ネイティブの<button>で発生する背景や枠線のリセットを避けられます。
components/common/PolymorphicGroupButtonの**PolymorphicGroupButton**:多態的で、cursor: pointerを設定するGroup(フレックス行)です。単一のボタンとして動作する横並びの要素に使用します。components/common/PolymorphicPaperButtonの**PolymorphicPaperButton**:多態的で、cursor: pointerを設定するPaper(カードの表面)です。カードのようなクリック可能な領域に使用します。
どちらも、ベースコンポーネント(GroupProps/PaperProps)のすべてのプロパティに加え、基底要素を指定するcomponentプロパティを受け取ります。
// ✅ Clickable row without native button style bleed
<PolymorphicGroupButton component="div" gap="sm" onClick={handleClick}>
<MantineIcon icon={IconFolder} />
<Text>Label</Text>
</PolymorphicGroupButton>
// ✅ Clickable card surface
<PolymorphicPaperButton component="div" p="md" onClick={handleClick}>
Card content
</PolymorphicPaperButton>
// ❌ Avoid - native <button> brings unwanted background/border in menus and panels
<UnstyledButton>
<Group>...</Group>
</UnstyledButton>
NumberInput
- 必ず
components/common/NumberInputのNumberInputを使用してください。MantineのNumberInputを直接使用しないでください。 - Mantine 8の
onChangeはnumber | stringを出力します(空のフィールド、-や12.のような入力途中の値、安全な範囲を超える大きな整数など)。ラッパーのonNumberChangeを使うと、numberまたはフィールドがクリアされたときのundefinedだけが渡されるため、一時的な文字列が出力されることはありません。 - デフォルトでは整数のみです。ラッパーのデフォルトは
decimalScale={0}です(多くのフィールドがポート、個数、タイムアウトであるため)。小数を入力するフィールドではdecimalScale={2}などを指定するか、上限をなくすにはdecimalScale="unlimited"を指定します。 onChangeプロパティは、フォームライブラリが解析を担当するform.getInputProps()のスプレッドでのみ引き続き使用できます。
// ✅ Good - cleared field maps to a domain decision at the call site
<NumberInput onNumberChange={(v) => setLimit(v ?? DEFAULT)} />
// ✅ Good - number-or-undefined sinks take the callback directly
<NumberInput decimalScale={2} onNumberChange={setThreshold} />
// ✅ OK - form spread owns value/onChange
<NumberInput {...form.getInputProps('warehouse.port')} />
// ❌ Avoid - hand-rolled typeof guards on the raw Mantine component
<MantineNumberInput onChange={(v) => { if (typeof v === 'number') setX(v); }} />
EmptyStateLoader
- ページレベルのガード、パネル、テーブル、空のコンテナーなど、すべての中央配置の読み込み状態には、
components/common/EmptyStateLoaderのEmptyStateLoaderを使用します。 SuboptimalState(Mantine v8)を基盤としており、任意のタイトル付きスピナーを親要素の中央に表示します。
TruncatedText
- 幅の制約によってテキストがはみ出す可能性がある場合は、
components/common/TruncatedTextのTruncatedTextを使用します。 maxWidth(数値または文字列)を渡して、切り詰める境界を指定します。- テキストが実際に切り詰められた場合にのみ、全文を表示するツールチップが自動的に表示されます(短い名前にはツールチップが表示されません)。
- デフォルトは
fz="sm"です。標準のTextプロパティで上書きできます。
// ✅ Good - truncates long names, tooltip only appears when needed
<TruncatedText maxWidth={200}>{item.name}</TruncatedText>
// ✅ Accepts any Text prop
<TruncatedText maxWidth="100%" fw={500}>{space.name}</TruncatedText>
検索、ページネーション、並べ替え機能付きテーブル
検索、ページネーション、並べ替え機能付きのテーブルには、components/common/ContentTableのContentTableコンポーネントを使用します。
フィルターが必要な場合は、FilterFacetを使用してください。
Mantineのドキュメント
LLM向けに最適化された形式で、すべてのコンポーネントとドキュメントへのリンクを掲載しています:https://mantine.dev/llms.txt