- name
- Shade use primitives
- description
- Replace bare divs that only carry flex/grid/gap utilities with Shade primitives (Stack, Inline, Box, Grid, Container, Text). Use semantic gap="md" instead of gap-4. Trigger when editing TSX in Shade-consuming apps.
- autoTrigger
- [{"fileEdit":"apps/{shade,admin,admin-x-framework,activitypub}/**/*.tsx"}]
# Shade — use primitives, not flex divs
A `<div>` whose className is only `flex / grid / gap / items-* / justify-*` utilities is a primitive call site. Use a Shade primitive instead so layouts read by **intent**, not by class string.
```ts
import {Stack, Inline, Box, Grid, Container, Text} from '@tryghost/shade/primitives';
```
## Picking the primitive
| You need | Use | Element |
| ------------------------------- | ----------- | ------------------------------------- |
| Column of children | `Stack` | flex-col |
| Row of children | `Inline` | flex-row (with optional `wrap`, `as`) |
| Padding / radius around content | `Box` | div with `padding`, `radius` props |
| Two-dimensional layout | `Grid` | display: grid |
| Width-constrained shell | `Container` | max-width wrapper |
| Text with size/tone/weight | `Text` | `as`, `size`, `tone`, `weight` |
## Use the semantic gap scale, not raw numbers
`gap="md"` reads as intent and resolves to the shared spacing scale. Mapping:
| Step | Tailwind | Use for |
| ------ | -------- | -------------------------- |
| `none` | `gap-0` | flush |
| `xs` | `gap-1` | inline icons-to-text |
| `sm` | `gap-2` | dense lists, badges |
| `md` | `gap-3` | default row/column spacing |
| `lg` | `gap-5` | section spacing |
| `xl` | `gap-6` | between major blocks |
| `2xl` | `gap-8` | page-level rhythm |
The same scale applies to `Box` padding (`padding="md"` → `p-3`).
## Correct
```tsx
<Box padding="lg" radius="md" className="border border-border-default">
<Inline align="center" gap="md" justify="between">
<Stack gap="xs">
<Text weight="semibold">Email notifications</Text>
<Text size="sm" tone="secondary">Get notified about engagement.</Text>
</Stack>
<Switch />
</Inline>
</Box>
```
## Incorrect
```tsx
// BAD — bare div doing flex layout
<div className="flex flex-col gap-2">
<div className="font-semibold">Title</div>
<div className="text-sm text-gray-600">Hint</div>
</div>
// BAD — raw gap-4 instead of gap="md"
<Stack className="gap-4">
```
## When NOT to reach for a primitive
- A pattern already exists for the shape (page header → `PageHeader`, list page → `ListPage`).
- The wrapper is starting to know about Ghost data — that's a Pattern, not a primitive composition.
- You're inside a Shade primitive's own implementation.
## className still works
Primitives forward `className` and merge with `cn()`. Use it for one-offs the prop API doesn't cover (e.g. `className="border border-border-default"`). Don't reach for `className` to set `flex`, `gap`, `align`, or `justify` — that's what the props are for.
View on GitHub