Back to Ghost

Shade — use primitives, not flex divs

.agents/skills/shade-use-primitives/SKILL.md

6.50.02.5 KB
Original Source

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 needUseElement
Column of childrenStackflex-col
Row of childrenInlineflex-row (with optional wrap, as)
Padding / radius around contentBoxdiv with padding, radius props
Two-dimensional layoutGriddisplay: grid
Width-constrained shellContainermax-width wrapper
Text with size/tone/weightTextas, size, tone, weight

Use the semantic gap scale, not raw numbers

gap="md" reads as intent and resolves to the shared spacing scale. Mapping:

StepTailwindUse for
nonegap-0flush
xsgap-1inline icons-to-text
smgap-2dense lists, badges
mdgap-3default row/column spacing
lggap-5section spacing
xlgap-6between major blocks
2xlgap-8page-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.