Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Guide for using Sentry's layout and text primitives. Use when implementing UI components, layouts, or typography. Enforces use of core components over styled components.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 271% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 279% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 182% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 206% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 139% | 0% |
ALWAYS use core components from @sentry/scraps instead of creating styled components with Emotion.
Core components provide consistent styling, responsive design, and better maintainability across the codebase.
For the complete list of supported props and their types, refer to the implementation files:
/static/app/components/core/layout/container.tsx - Base container with all layout propsflex.tsx - Flex layout primitivegrid.tsx - Grid layout primitivestack.tsx - Stack layout primitive (Flex with column direction by default)/static/app/components/core/text/text.tsx - Text primitiveheading.tsx - Heading primitive> Important: Flex, Grid, and Stack all extend Container. This means every prop available on Container is also available on Flex, Grid, and Stack. When you use <Flex>, you get all Container props (position, padding, border, overflow, etc.) PLUS the flex-specific props. The same applies to Grid and Stack.
Base layout component that supports all common layout properties. Flex, Grid, and Stack extend Container, inheriting all of its props.
Key Props (see container.tsx for complete list):
position: "static" | "relative" | "absolute" | "fixed" | "sticky"padding, paddingTop, paddingBottom, paddingLeft, paddingRight: SpaceSize tokensmargin, marginTop, etc.: SpaceSize tokens (deprecated, prefer gap)width, height, minWidth, maxWidth, minHeight, maxHeightborder, borderTop, borderBottom, borderLeft, borderRight: BorderVariant tokensradius: RadiusSize tokensoverflow, overflowX, overflowY: "visible" | "hidden" | "scroll" | "auto"background: SurfaceVariant ("primary" | "secondary" | "tertiary")display: Various display typesflex, flexGrow, flexShrink, flexBasis, alignSelf, orderarea, row, columntsximport {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>;
Use <Flex> for flex layouts. Extends Container, inheriting all Container props plus flex-specific props.
Flex-Specific Props (see flex.tsx for complete list):
direction: "row" | "row-reverse" | "column" | "column-reverse"align: "start" | "end" | "center" | "baseline" | "stretch"justify: "start" | "end" | "center" | "between" | "around" | "evenly" | "left" | "right"gap: SpaceSize or "${SpaceSize} ${SpaceSize}" for row/column gapwrap: "nowrap" | "wrap" | "wrap-reverse"display: "flex" | "inline-flex" | "none"Plus ALL Container props: position, padding, margin, width, height, border, radius, overflow, background, flex/grid item props, and more (see Container section above).
tsximport {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>;
Use <Grid> for grid layouts. Extends Container, inheriting all Container props plus grid-specific props.
Grid-Specific Props (see grid.tsx for complete list):
columns: Grid template columns (number or CSS value)rows: Grid template rowsareas: Named grid areasgap: SpaceSize or "${SpaceSize} ${SpaceSize}" for row/column gapalign: "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: Size of auto-generated tracksPlus ALL Container props: position, padding, margin, width, height, border, radius, overflow, background, flex/grid item props, and more (see Container section above).
tsximport {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>;
Use <Stack> for vertical layouts. Stack is essentially Flex with direction="column" by default. It also provides Stack.Separator for adding separators between items.
Props (see stack.tsx for complete list):
direction defaults to "column" (but can be overridden)Stack.Separator component for adding dividers between stack itemstsximport {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>;
Use <Text> for all text content. Never use raw <p>, <span>, or <div> elements with text styling.
Key Props (see text.tsx for complete list):
as: "span" | "p" | "label" | "div" (semantic HTML element)size: TextSize ("xs" | "sm" | "md" | "lg" | "xl" | "2xl")variant: ContentVariant | "muted" (see Content Variant Tokens below)align: "left" | "center" | "right" | "justify"bold: booleanitalic: booleanuppercase: booleanmonospace: booleantabular: boolean (fixed-width numbers)ellipsis: boolean (truncate with ellipsis)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" (line-height)underline: boolean | "dotted"strikethrough: booleantsximport {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>
Use <Heading> for all headings. Never use raw <h1>, <h2>, etc. elements.
Key Props (see heading.tsx for complete list):
as: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" (REQUIRED)size: HeadingSize ("xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl")variant: Same as Textalign: Same as Textitalic, monospace, tabular: Same as Textellipsis, wrap, textWrap, wordBreak: Same as Textdensity: Same as Textunderline, strikethrough: Same as TextNote: bold and uppercase are NOT available on Heading (headings are always bold).
tsximport {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>
> Important: Always prefer InfoTip and InfoText over using raw <Tooltip> components. These provide consistent, accessible patterns for contextual help.
Use <InfoTip> to add an info icon with tooltip next to labels or headings. It's keyboard accessible and provides a consistent pattern for supplementary help.
tsximport {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>
Key Props:
title: Tooltip content (required)size: "xs" | "sm" (default) | "md"When to Use:
Use <InfoText> for inline text with a tooltip. It renders text with a dotted underline that reveals a tooltip on hover/focus.
tsximport {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>
Key Props:
title: Tooltip content (required)Text, so supports all Text props: size, variant, bold, etc.tsx// With Text styling props <InfoText title="Small muted text" size="sm" variant="muted"> Hint text </InfoText> <InfoText title="Bold text" bold> Important term </InfoText>
When to Use:
> ⚠️ CRITICAL: ALWAYS prompt the user for confirmation before creating abstractions over layout primitives (Container, Flex, Grid, Stack, Text, Heading) when the intent is to DRY (Don't Repeat Yourself) repeated props.
You can create thin abstractions over primitives with the purpose of improving the semantic structure by using meaningful names (e.g., TableCell vs generic Flex) and with the purpose of providing some default props. It is very important that you do this sparingly, and only when it is a net gain for readability. For example, if there are only two instances of the duplicated props, and they are placed next to each other, the price of the abstraction outweights the terseness.
Before creating an abstraction, you MUST:
tsximport {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 */}
Key points:
extends FlexProps){...props} to allow overridesMost props support responsive syntax using breakpoint keys.
tsx// ❌ 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'}}>
Responsive prop keys come in two flavors, and may be combined on one prop:
{xs: …}) resolve against the nearest query container —container queries are the default, so they need no prefix.
screen:-prefixed keys ({'screen:md': …}) resolve against theviewport.
To react to a component's available space, declare a parent as a query container with containerType="inline-size" and use bare keys on the child (an element can never query its own size, so the container must be an ancestor).
tsx// ✅ Reflow based on the parent's width, not the viewport's <Container containerType="inline-size"> <Flex direction={{'2xs': 'column', md: 'row'}}>{/* ... */}</Flex> </Container>
When you need the resolved breakpoint in JS, use useContainerBreakpoint(ref) (the ResizeObserver-backed, container-scoped replacement for width-based useMedia).
Container supports margin props but they are deprecated. Use gap on parent containers instead.
tsx// ❌ 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>;
Don't couple layout and typography in a single styled component. Use separate primitives.
tsx// ❌ 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>;
The implementation files contain the complete, up-to-date list of supported props with TypeScript types. When in doubt:
/static/app/components/core/layout/container.tsx for base layout props/static/app/components/core/layout/flex.tsx for Flex-specific props/static/app/components/core/layout/grid.tsx for Grid-specific props/static/app/components/core/layout/stack.tsx for Stack-specific props/static/app/components/core/text/text.tsx for Text props/static/app/components/core/text/heading.tsx for Heading propsUse these for gap, padding:
"0", "2xs", "xs", "sm", "md", "lg", "xl", "2xl", "3xl""md lg" (vertical horizontal){{xs: "sm", md: "lg"}}Use these for border prop:
"primary", "muted", "accent", "danger", "promotion", "success", "warning"Use these for radius prop:
"0", "2xs", "xs", "sm", "md", "lg", "xl", "2xl", "full"Use these for background prop on layout components:
"primary", "secondary", "tertiary"Use these for variant prop on Text and Heading:
Before creating a styled component, ask:
<Flex>, <Grid>, or <Stack> for layout?<Stack> for vertical layouts with default column direction?<Container> for borders/padding/positioning?<Text> or <Heading> for typography?<InfoTip> or <InfoText> instead of <Tooltip>?gap instead of margins?If you answered yes to any of these, use the primitive instead.
Use the core avatar components (<UserAvatar/>, <TeamAvatar/>, <ProjectAvatar/>, <OrganizationAvatar/>, <SentryAppAvatar/>, <DocIntegrationAvatar/>) from static/app/components/core/avatar. For lists of avatars, use <AvatarList>.
tsx// ✅ 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 the core disclosure component instead of building your own.
tsx// ✅ 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> ); }
Place all icons in the static/app/icons folder. Never inline SVGs or add them to any other folder. Optimize SVGs using svgo or svgomg.
tsx// ❌ 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"
All images belong inside static/app/images and must be imported via the webpack loader (the sentry-images alias), never referenced by static path.
tsx// ✅ 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" />; }
Other measured skills in the registry, with their headline benchmark lift.