design-system について
Sentry のフロントエンド開発者向けに、`@sentry/scraps` のレイアウト、テキスト、情報表示プリミティブの使い方を案内します。独自のスタイル付きコンポーネントではなく、共通コンポーネントとレスポンシブプロパティの利用を重視します。
主な機能
- 01Container、Flex、Grid、Stack のレイアウトプリミティブ
- 02Text と Heading のタイポグラフィプリミティブ
- 03InfoTip と InfoText による補足説明
- 04コンテナークエリー対応のレスポンシブプロパティ
- 05抽象化の作成前に確認を必須化
活用シーン
- 01一貫性のある Sentry ページレイアウトの構築
- 02テキストや見出しへの共通スタイル適用
- 03アクセシブルなヒントや用語説明の追加
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ getsentry/ sentry --skill design-systemインストール後、Claude Code で/design-systemを入力して読み込まれたか確認します。
詳細
Sentry におけるレイアウトとテキストのプリミティブ
基本原則
Emotion でスタイル付きコンポーネントを作成する代わりに、常に @sentry/scraps のコアコンポーネントを使用してください。
コアコンポーネントは、コードベース全体で一貫したスタイル、レスポンシブデザイン、および優れた保守性を提供します。
コンポーネント実装リファレンス
サポートされている props の完全なリストとその型については、実装ファイルを参照してください。
- レイアウトコンポーネント:
/static/app/components/core/layout/container.tsx- すべてのレイアウト props を備えたベースコンテナflex.tsx- Flex レイアウトプリミティブgrid.tsx- Grid レイアウトプリミティブstack.tsx- Stack レイアウトプリミティブ(デフォルトで column 方向の Flex)
- タイポグラフィコンポーネント:
/static/app/components/core/text/text.tsx- テキストプリミティブheading.tsx- 見出しプリミティブ
レイアウトプリミティブ
重要:
Flex、Grid、StackはすべてContainerを拡張しています。つまり、Container で利用可能なすべての prop は、Flex、Grid、Stack でも利用可能です。<Flex>を使用すると、すべての Container props(position、padding、border、overflow など)に加えて、Flex 固有の props を利用できます。同じことが Grid と Stack にも当てはまります。
Container
すべての一般的なレイアウトプロパティをサポートするベースレイアウトコンポーネントです。Flex、Grid、Stack は Container を拡張し、そのすべての props を継承します。
主要な Props(完全なリストは container.tsx を参照):
position: "static" | "relative" | "absolute" | "fixed" | "sticky"padding、paddingTop、paddingBottom、paddingLeft、paddingRight: SpaceSize トークンmargin、marginTopなど: SpaceSize トークン(非推奨、gap を推奨)width、height、minWidth、maxWidth、minHeight、maxHeightborder、borderTop、borderBottom、borderLeft、borderRight: BorderVariant トークンradius: RadiusSize トークンoverflow、overflowX、overflowY: "visible" | "hidden" | "scroll" | "auto"background: SurfaceVariant ("primary" | "secondary" | "tertiary")display: 各種 display タイプ- Flex アイテム props:
flex、flexGrow、flexShrink、flexBasis、alignSelf、order - Grid アイテム props:
area、row、column
import {Container} from '@sentry/scraps/layout';
// ❌ Don't create styled components
const Component = styled('div')`
padding: ${p => p.theme.space.md};
border: 1px solid ${p => p.theme.tokens.border.primary};
`;
// ✅ Use Container primitive
<Container padding="md" border="primary">
Content
</Container>;
Flex
Flex レイアウトには <Flex> を使用します。Container を拡張し、すべての Container props に加えて Flex 固有の props を継承します。
Flex 固有の Props(完全なリストは flex.tsx を参照):
direction: "row" | "row-reverse" | "column" | "column-reverse"align: "start" | "end" | "center" | "baseline" | "stretch"justify: "start" | "end" | "center" | "between" | "around" | "evenly" | "left" | "right"gap: SpaceSize または行/列の gap には"${SpaceSize} ${SpaceSize}"wrap: "nowrap" | "wrap" | "wrap-reverse"display: "flex" | "inline-flex" | "none"
さらにすべての Container props: position、padding、margin、width、height、border、radius、overflow、background、flex/grid アイテム props など(上記の Container セクションを参照)。
import {Flex} from '@sentry/scraps/layout';
// ❌ Don't create styled components
const Component = styled('div')`
display: flex;
flex-direction: column;
position: relative;
`;
// ✅ Use Flex primitive with props
<Flex direction="column" position="relative" gap="md">
<Child1 />
<Child2 />
</Flex>;
Grid
Grid レイアウトには <Grid> を使用します。Container を拡張し、すべての Container props に加えて Grid 固有の props を継承します。
Grid 固有の Props(完全なリストは grid.tsx を参照):
columns: Grid テンプレート列(数値または CSS 値)rows: Grid テンプレート行areas: 名前付き Grid エリアgap: SpaceSize または行/列の gap には"${SpaceSize} ${SpaceSize}"align: "start" | "end" | "center" | "baseline" | "stretch"(align-items)alignContent: "start" | "end" | "center" | "between" | "around" | "evenly" | "stretch"justify: "start" | "end" | "center" | "between" | "around" | "evenly" | "stretch"(justify-content)justifyItems: "start" | "end" | "center" | "stretch"flow: "row" | "column" | "row dense" | "column dense"autoColumns、autoRows: 自動生成されるトラックのサイズ
さらにすべての Container props: position、padding、margin、width、height、border、radius、overflow、background、flex/grid アイテム props など(上記の Container セクションを参照)。
import {Grid} from '@sentry/scraps/layout';
// ❌ Don't create styled components
const Component = styled('div')`
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: ${p => p.theme.space.md};
`;
// ✅ Use Grid primitive
<Grid columns="repeat(3, 1fr)" gap="md">
<Item1 />
<Item2 />
<Item3 />
</Grid>;
スタック
縦方向のレイアウトには <Stack> を使用します。Stack は基本的に Flex で、デフォルトで direction="column" が設定されています。また、項目間にセパレーターを追加するための Stack.Separator も提供しています。
Props(完全なリストについては stack.tsx を参照してください):
- Flex の Props と同じ(Flex と Container のすべての Props を継承します)
directionのデフォルトは "column"(ただし上書き可能)- スタック項目間に区切り線を追加するための
Stack.Separatorコンポーネント
import {Stack} from '@sentry/scraps/layout';
// ❌ Don't create styled components for vertical layouts
const Component = styled('div')`
display: flex;
flex-direction: column;
gap: ${p => p.theme.space.md};
`;
// ✅ Use Stack primitive (automatically column direction)
<Stack gap="md">
<Item1 />
<Item2 />
<Item3 />
</Stack>;
// ✅ With separators between items
<Stack gap="md">
<Item1 />
<Stack.Separator />
<Item2 />
<Stack.Separator />
<Item3 />
</Stack>;
// ✅ Stack supports all Flex and Container props
<Stack gap="md" padding="lg" position="relative" border="primary">
<Item1 />
<Item2 />
</Stack>;
タイポグラフィプリミティブ
Text
すべてのテキストコンテンツには <Text> を使用します。テキストのスタイリングを施した生の <p>、<span>、<div> 要素は決して使用しないでください。
主な Props(完全なリストについては text.tsx を参照してください):
as: "span" | "p" | "label" | "div"(セマンティックな HTML 要素)size: TextSize ("xs" | "sm" | "md" | "lg" | "xl" | "2xl")variant: ContentVariant | "muted"(後述のコンテンツバリアントトークンを参照)align: "left" | "center" | "right" | "justify"bold: booleanitalic: booleanuppercase: booleanmonospace: booleantabular: boolean(固定幅の数字)ellipsis: boolean(省略記号で切り詰め)wrap: "nowrap" | "normal" | "pre" | "pre-line" | "pre-wrap"textWrap: "wrap" | "nowrap" | "balance" | "pretty" | "stable"wordBreak: "normal" | "break-all" | "keep-all" | "break-word"density: "compressed" | "comfortable"(行の高さ)underline: boolean | "dotted"strikethrough: boolean
import {Text} from '@sentry/scraps/text';
// ❌ Don't create styled text components
const Label = styled('span')`
color: ${p => p.theme.tokens.content.secondary};
font-size: ${p => p.theme.font.size.sm};
`;
// ❌ Don't use raw elements
<p>This is a paragraph</p>
<span>Status: Active</span>
// ✅ Use Text primitive with semantic 'as' prop
<Text as="p" variant="muted" density="comfortable">
This is a paragraph
</Text>
<Text as="span" bold uppercase>
Status: Active
</Text>
Heading
すべての見出しには <Heading> を使用します。生の <h1>、<h2> などの要素は決して使用しないでください。
主な Props(完全なリストについては heading.tsx を参照してください):
as: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"(必須)size: HeadingSize ("xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl")variant: Text と同じalign: Text と同じitalic、monospace、tabular: Text と同じellipsis、wrap、textWrap、wordBreak: Text と同じdensity: Text と同じunderline、strikethrough: Text と同じ
注: bold と uppercase は Heading では使用できません(見出しは常に太字です)。
import {Heading} from '@sentry/scraps/text';
// ❌ Don't style heading elements
const Title = styled('h2')`
font-size: ${p => p.theme.font.size.md};
font-weight: bold;
`;
// ❌ Don't use raw heading elements
<h2>My Title</h2>
// ✅ Use Heading primitive with semantic 'as' prop
<Heading as="h2">My Title</Heading>
// ✅ With custom size
<Heading as="h3" size="xl">Large H3</Heading>
情報コンポーネント
重要: 生の
<Tooltip>コンポーネントを使用するよりも、常にInfoTipとInfoTextを優先してください。これらは、状況に応じたヘルプのための一貫性のあるアクセシブルなパターンを提供します。
InfoTip
ラベルや見出しの横に、ツールチップ付きの情報アイコンを追加するには <InfoTip> を使用します。キーボードでアクセス可能で、補足的なヘルプのための一貫したパターンを提供します。
import {InfoTip} from '@sentry/scraps/info';
import {Flex} from '@sentry/scraps/layout';
import {Text} from '@sentry/scraps/text';
// ❌ Don't use Tooltip with arbitrary icons
<Flex gap="xs" align="center">
<Text>Retention Period</Text>
<Tooltip title="The number of days...">
<IconInfo size="xs" />
</Tooltip>
</Flex>
// ✅ Use InfoTip for contextual help icons
<Flex gap="xs" align="center">
<Text>Retention Period</Text>
<InfoTip title="The number of days event data is stored before being automatically deleted." />
</Flex>
主な Props:
title: ツールチップの内容(必須)size: "xs" | "sm"(デフォルト) | "md"
使用する場面:
- 見出しやセクションタイトルにコンテキストを追加する
- インラインテキストなしで補足情報を表示する
- 設定や構成オプションを説明する
InfoText
ツールチップ付きのインラインテキストには <InfoText> を使用します。ホバーまたはフォーカス時にツールチップを表示する点線の下線付きでテキストをレンダリングします。
import {InfoText} from '@sentry/scraps/info';
// ❌ Don't wrap text with raw Tooltip
<Tooltip title="Time to First Byte measures the time...">
<span style={{textDecoration: 'underline dotted'}}>TTFB</span>
</Tooltip>
// ✅ Use InfoText for inline explanations
<InfoText title="Time to First Byte measures the time from the request start until the first byte of the response is received.">
TTFB
</InfoText>
主な Props:
title: ツールチップの内容(必須)Textを拡張しているため、すべての Text の Props(size、variant、boldなど)をサポートします。
// With Text styling props
<InfoText title="Small muted text" size="sm" variant="muted">
Hint text
</InfoText>
<InfoText title="Bold text" bold>
Important term
</InfoText>
使用する場面:
- 技術用語や頭字語をインラインで定義する
- 視覚的な煩雑さを加えずに追加のコンテキストを提供する
- 一貫性のあるアクセシブルなインラインヘルプのパターンを作成する
薄い抽象化の作成
⚠️ 重要: レイアウトプリミティブ(
Container、Flex、Grid、Stack、Text、Heading)に対する抽象化を、繰り返し指定される props の DRY(Don't Repeat Yourself)を目的として作成する場合は、必ず事前にユーザーに確認を求めてください。
意味のある名前(例: 汎用的な Flex ではなく TableCell)を使うことでセマンティックな構造を改善する目的や、いくつかのデフォルト props を提供する目的で、プリミティブに対する薄い抽象化を作成できます。ただし、これは控えめに行い、可読性にとって純粋な利益となる場合にのみ行うことが非常に重要です。たとえば、重複した props のインスタンスが 2 つしかなく、それらが隣り合って配置されている場合、抽象化のコストが簡潔さを上回ります。
抽象化を作成する前に、必ず次のことを行ってください:
- ユーザーに確認を求める
- 作成する予定の抽象化について説明する
- その抽象化が追加される複雑さに見合う価値がある理由を説明する
- 続行する前に明示的な承認を待つ
import {Flex, type FlexProps} from '@sentry/scraps/layout';
// ❌ Don't repeat the same props everywhere
<Flex align="center" gap="xs" flex="1" padding="sm">Content 1</Flex>
<Flex align="center" gap="xs" flex="1" padding="sm">Content 2</Flex>
<Flex align="center" gap="xs" flex="1" padding="sm">Content 3</Flex>
<Flex align="center" gap="xs" flex="1" padding="sm">Content 4</Flex>
// ✅ Create a thin wrapper with default props (AFTER USER CONFIRMATION)
function TableCell(props: FlexProps) {
return <Flex align="center" gap="md" {...props} />;
}
<TableCell>Content 1</TableCell>
<TableCell>Content 2</TableCell>
<TableCell align="start">Content 3</TableCell>{/* Can override defaults */}
重要なポイント:
- 抽象化を作成する前に必ずユーザーの確認を求めてください
- プリミティブの props 型を拡張します(
extends FlexProps) - JSX コンポーネントにデフォルトを設定し、
{...props}を展開して上書きを可能にします - styled components は使わず、代わりにプリミティブを組み合わせてください
一般的なガイドライン
1. レスポンシブ props を使用する
ほとんどの props は、ブレークポイントキーを使ったレスポンシブ構文をサポートしています。
// ❌ Don't use styled media queries
const Component = styled('div')`
display: flex;
flex-direction: column;
@media screen and (min-width: ${p => p.theme.breakpoints.md}) {
flex-direction: row;
}
`;
// ✅ Use responsive prop signature. `screen:` keys match the viewport, like the
// @media query above.
<Flex direction={{'screen:xs': 'column', 'screen:md': 'row'}}>
レスポンシブ props のキーには 2 種類があり、1 つの prop に組み合わせることができます:
- ベアキー(
{xs: …})は最も近いクエリコンテナに対して解決されます — コンテナクエリがデフォルトであるため、プレフィックスは不要です。 screen:プレフィックス付きキー({'screen:md': …})は ビューポートに対して解決されます。
コンポーネントの利用可能なスペースに反応させるには、親を containerType="inline-size" でクエリコンテナとして宣言し、子でベアキーを使用します(要素が自身のサイズをクエリすることはできないため、コンテナは祖先である必要があります)。
// ✅ Reflow based on the parent's width, not the viewport's
<Container containerType="inline-size">
<Flex direction={{'2xs': 'column', md: 'row'}}>{/* ... */}</Flex>
</Container>
JS で解決されたブレークポイントが必要な場合は、useContainerBreakpoint(ref) を使用します(幅ベースの useMedia に代わる、ResizeObserver を利用したコンテナスコープのもの)。
2. マージンよりもギャップ/パディングを優先する
Container は margin props をサポートしていますが、非推奨です。代わりに親コンテナで gap を使用してください。
// ❌ Don't use margin between children
const Child = styled('div')`
margin-right: ${p => p.theme.space.lg};
`;
// ✅ Use gap on parent container
<Flex gap="lg">
<Child1 />
<Child2 />
</Flex>;
3. レイアウトとタイポグラフィを分離する
単一の styled component でレイアウトとタイポグラフィを結合しないでください。別々のプリミティブを使用してください。
// ❌ Don't couple layout and typography
const Component = styled('div')`
display: flex;
flex-direction: column;
color: ${p => p.theme.tokens.content.secondary};
font-size: ${p => p.theme.font.size.lg};
`;
// ✅ Split into layout and typography primitives
<Flex direction="column">
<Text variant="muted" size="lg">
Content
</Text>
</Flex>;
4. すべての props については実装ファイルを確認する
実装ファイルには、サポートされている props の完全で最新のリストが TypeScript の型とともに含まれています。不明な場合は:
- 基本レイアウト props については
/static/app/components/core/layout/container.tsxを読みます - Flex 固有の props については
/static/app/components/core/layout/flex.tsxを読みます - Grid 固有の props については
/static/app/components/core/layout/grid.tsxを読みます - Stack 固有の props については
/static/app/components/core/layout/stack.tsxを読みます - Text の props については
/static/app/components/core/text/text.tsxを読みます - Heading の props については
/static/app/components/core/text/heading.tsxを読みます
トークンリファレンス
スペーシングトークン(SpaceSize)
gap、padding に使用します:
"0"、"2xs"、"xs"、"sm"、"md"、"lg"、"xl"、"2xl"、"3xl"- 複数値:
"md lg"(垂直 水平) - レスポンシブ:
{{xs: "sm", md: "lg"}}
ボーダートークン(BorderVariant)
border prop に使用します:
"primary"、"muted"、"accent"、"danger"、"promotion"、"success"、"warning"
ラジアストークン(RadiusSize)
radius prop に使用します:
"0"、"2xs"、"xs"、"sm"、"md"、"lg"、"xl"、"2xl"、"full"
テキストサイズトークン
- TextSize: "xs" | "sm" | "md" | "lg" | "xl" | "2xl"
- HeadingSize: "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl"
サーフェスバリアントトークン(SurfaceVariant)
レイアウトコンポーネントの background prop に使用します:
"primary"、"secondary"、"tertiary"
Content Variant トークン(ContentVariant)
Text と Heading の variant プロパティには、以下を使用します。
- ContentVariant: "primary" | "secondary" | "accent" | "danger" | "promotion" | "success" | "warning"
- 「muted」も指定可能: Text と Heading では、ContentVariant の値に加えて "muted" も指定できます
クイックリファレンスチェックリスト
スタイル付きコンポーネントを作成する前に、次の点を確認してください。
- ✅ レイアウトに
<Flex>、<Grid>、または<Stack>を使用できますか? - ✅ デフォルトの列方向で縦方向にレイアウトするには、
<Stack>を使用できますか? - ✅ 枠線、パディング、配置には
<Container>を使用できますか? - ✅ タイポグラフィには
<Text>または<Heading>を使用できますか? - ✅
<Tooltip>の代わりに<InfoTip>または<InfoText>を使用できますか? - ✅ メディアクエリの代わりにレスポンシブプロパティを使用できますか?
- ✅ マージンの代わりに
gapを使用できますか? - ✅ プリミティブは必要なプロパティに対応していますか?(実装ファイルを確認してください)
いずれかに「はい」と答えた場合は、代わりにそのプリミティブを使用してください。
その他のコアコンポーネント
メニューとセレクト
アクションメニューには DropdownMenu を、選択肢には CompactSelect を使用します。フォームでは、フォームシステムの field.Select または field.CompactSelect を使用します。@sentry/scraps/menu の Menu は内部用のプレゼンテーションプリミティブです。DropdownMenu や CompactSelect などの共通コントロールを実装する場合に限り、直接使用してください。
アバター
static/app/components/core/avatar のコアアバターコンポーネント(<UserAvatar/>、<TeamAvatar/>、<ProjectAvatar/>、<OrganizationAvatar/>、<SentryAppAvatar/>、<DocIntegrationAvatar/>)を使用します。アバターのリストには <AvatarList> を使用してください。
// ✅ Use Avatar component and useUser
import {UserAvatar} from '@sentry/scraps/avatar';
import {useUser} from 'sentry/utils/useUser';
<UserAvatar user={user}>
// ❌ Do not use raw intrinsic elements or static paths
function Component() {
return (
<img
src="/path/to/image.jpg"
style={{
border,
width: 20,
height: 20,
borderRadius: '50%',
objectFit: 'cover',
display: 'inline-block',
}}
/>
);
}
開閉表示
独自に作成する代わりに、コアの開閉表示コンポーネントを使用してください。
// ✅ Use Disclosure component
<Disclosure>
<Disclosure.Title>Title</Disclosure.Title>
<Disclosure.Content>Content that is toggled based on expanded state</Disclosure.Content>
</Disclosure>;
// ❌ Do not reimplement disclosure pattern manually
function Component() {
const [isExpanded, setIsExpanded] = useState(false);
return (
<div>
<Button
onClick={() => setIsExpanded(!isExpanded)}
icon={<IconChevron direction={isExpanded ? 'down' : 'right'} />}
>
Title
</Button>
{isExpanded && (
<Container>Content that is toggled based on expanded state</Container>
)}
</div>
);
}
画像とアイコン
SvgIcon ベースのアイコンは static/packages/icons/src に配置し、明示的な @sentry/icons のサブパスからインポートします。アプリのアセット用ラッパーは static/app/icons に配置します。icons パッケージは scraps やアプリのコードに依存してはいけません。SVG をインラインで記述しないでください。svgo または svgomg を使用して SVG を最適化してください。
// ❌ Never inline SVGs
function Component(){
return (
<Button icon={
<svg viewbox="0 0 16 16>"}>
// ❌ paths have excessive precision, optimize them with SVGO
<circle cx="8.00134" cy="8.4314" r="5.751412" />
<circle cx="8.00134" cy="8.4314" r="12.751412" />
<line x1="8.41334" y1="5.255361" x2="8" y2="8.255421" />
</svg>
</Button>
)
}
// ❌ Never place SVGs outside of icons folder.
import {CustomIcon} from "./customIcon"
// ✅ Import icon from our icon set
import {IconExclamation} from "@sentry/icons/exclamation"
すべての画像は static/app/images 内に配置し、webpack ローダー(sentry-images エイリアス)経由でインポートする必要があります。静的パスから直接参照しないでください。
// ✅ Images are imported from sentry-images alias
import image from 'sentry-images/example.png';
function Component() {
return <Image src={image} />;
}
// ❌ All images need to be imported using the webpack loader!
function Component() {
return <Image src="/path/to/image.png" />;
}