Email primitives
Every component @cascivo/email exports, with its props and a worked example. Live previews of
all 22, rendered as real email documents, are at https://cascivo.com/docs/email/components. The
full set of 12 themes is demonstrated on complete templates at
https://cascivo.com/docs/email.
These are not the @cascivo/react components. HTML email is a separate render target:
no flexbox, no grid, no custom properties, no rem, no client JavaScript. Layout is tables and
styling is inline, because Outlook Windows still renders through Microsoft Word. See
RECIPE-EMAIL.md for the whole workflow and
EMAIL-CLIENT-SUPPORT.md for what the conformance lint enforces.
import { Body, Button, Container, Head, Html, Preview, Section, Text } from '@cascivo/email'
Every primitive below also takes children (its content) and style — inline declarations merged over the primitive's own, in camelCase, with lengths written in px.
Contents
- Document —
Html,Head,Body,Preview,Style - Layout —
Container,Section,Row,Column,Spacer,Hr - Typography —
Heading,Text,Link,List,Footer - Content —
Button,Card,Alert,Badge,Img,Markdown
Document
Html
The root element.
| Prop | Type | Default | Notes |
|---|---|---|---|
children | ReactNode | — | |
lang | string | 'en' | BCP 47 language tag. Also set on <body>, because some clients strip <html>. |
dir | 'ltr' | 'rtl' | 'ltr' | Text direction. rtl flips the reading order; the primitives use no physical properties that would fight it. |
The document shell
Every email starts here. Html carries the XHTML doctype, the namespaces Outlook needs for VML, and the lang/dir pair that a right-to-left message sets once.
<Html lang="en" dir="ltr">
<Head title="Your receipt" />
<Body>
<Container>
<Section padding={24}>
<Heading level={2}>Your receipt</Heading>
<Text>Thanks — your payment went through.</Text>
</Section>
</Container>
</Body>
</Html>
Head
Document head.
| Prop | Type | Default | Notes |
|---|---|---|---|
children | ReactNode | — | |
title | string | — | Rendered as <title>. Some clients show it; most ignore it. |
Title and client meta
The title is what a browser tab and several webmail clients show. Everything else a mail client needs — the viewport tag, the colour-scheme pair, the Outlook pixel-density fix — is emitted for you.
<Html>
<Head title="Reset your password" />
<Body>
<Container>
<Section padding={24}>
<Text>Check the tab title.</Text>
</Section>
</Container>
</Body>
</Html>
Body
Document body, painted with the theme background.
| Prop | Type | Default | Notes |
|---|---|---|---|
children | ReactNode | — | |
style | Style | — | |
lang | string | 'en' | Repeated from Html — several clients strip <html> and graft the body into their own document. |
dir | 'ltr' | 'rtl' | 'ltr' | Repeated from Html, for the same reason lang is. |
Themed background
The background and foreground come from the theme, resolved to literal sRGB — no client supports a custom property. Switch the theme above and this frame is the only thing that has to change.
<Html>
<Head title="Body" />
<Body>
<Container>
<Section padding={24}>
<Heading level={3}>Painted by the theme</Heading>
<Text variant="muted">Twelve themes, one component tree.</Text>
</Section>
</Container>
</Body>
</Html>
Preview
The inbox preview text — the grey line beside the subject.
| Prop | Type | Default | Notes |
|---|---|---|---|
children (required) | string | — | The inbox preview line. Aim for under ~90 characters; clients truncate past that. |
The inbox line
Deliberately invisible in the frame — it is the grey line the inbox shows beside the subject. Without it the client repeats the opening words of the body instead. Keep it under ~90 characters.
<Html>
<Head title="Reset your password" />
<Body>
<Preview>Reset your password — the link expires in 30 minutes</Preview>
<Container>
<Section padding={24}>
<Heading level={2}>Reset your password</Heading>
<Text>The link below expires in 30 minutes.</Text>
</Section>
</Container>
</Body>
</Html>
Style
A <style> block, hoisted into <head> by the renderer.
| Prop | Type | Default | Notes |
|---|---|---|---|
children (required) | string | — | CSS text. Written verbatim — it is authored, not user input. |
A rule inline styles cannot express
Media queries and pseudo-classes are the only two things that need a stylesheet. Declare the rule next to the component that needs it — the renderer hoists every block into <head> and collapses duplicates.
<Html>
<Head title="Style" />
<Body>
<Container>
<Section padding={24}>
<Style>
{'@media only screen and (max-width:600px){.hero{font-size:24px!important}}'}
</Style>
<Text className="hero" size="32px">
Big on desktop, smaller on a phone
</Text>
</Section>
</Container>
</Body>
</Html>
Layout
Container
Centred fixed-width column — the outermost content wrapper.
| Prop | Type | Default | Notes |
|---|---|---|---|
children | ReactNode | — | |
width | number | CONTENT_WIDTH | Content width in pixels. |
responsive | boolean | true | Emit the width override that lets the container go fluid on a phone. |
breakpoint | number | — | Viewport width, in pixels, below which the container goes fluid. |
className | string | — | Extra class, for a rule of your own in a Style block. |
style | Style | — |
The centred column
Centred by both margin: 0 auto and align="center", because neither works in every client. The default width is 600px, which clears every desktop reading pane; this one is narrowed to 400 so the centring is visible. responsive adds the width override that stops a phone scrolling sideways.
<Container width={400}>
<Section padding={24}>
<Card padding={16}>
<Text align="center">400px, centred</Text>
</Card>
</Section>
</Container>
Section
A full-width band of content.
| Prop | Type | Default | Notes |
|---|---|---|---|
children | ReactNode | — | |
padding | number | string | 0 | Vertical padding in pixels, applied to the cell. |
background | string | — | Band colour. A literal — no client resolves a custom property, so pass a hex or call token(). |
align | 'left' | 'center' | 'right' | — | Horizontal alignment of the cell's content, emitted as the align attribute so nested tables follow it too. |
className | string | — | Extra class, for a rule of your own in a Style block. |
style | Style | — |
Bands of content
One table, one cell — padding on the cell, because Outlook drops padding on a <div>. A background makes it a full-bleed band.
<Container>
<Section padding={32} background="#1f2937" align="center">
<Heading level={2} align="center" style={{ color: '#ffffff' }}>
Acme
</Heading>
</Section>
<Section padding={24}>
<Text>And an unpainted band below it, with its own padding.</Text>
</Section>
</Container>
Row
A horizontal group of Columns.
| Prop | Type | Default | Notes |
|---|---|---|---|
children | ReactNode | — | |
className | string | — | Extra class, for a rule of your own in a Style block. |
style | Style | — |
Columns side by side
A <tr> that never inspects its children — it emits the row and trusts each Column to emit its cell. Flexbox and grid are unsupported in Outlook Windows; this is the layout primitive.
<Row>
<Column width="50%" padding={8}>
<Text>Ordered</Text>
</Column>
<Column width="50%" padding={8} align="right">
<Text>3 items</Text>
</Column>
</Row>
Column
One cell of a Row.
| Prop | Type | Default | Notes |
|---|---|---|---|
children | ReactNode | — | |
width | number | string | — | Width as a percentage or pixel count. Set both attribute and style. |
align | 'left' | 'center' | 'right' | — | Horizontal alignment of the cell's content. A Button with no align of its own follows this. |
valign | 'top' | 'middle' | 'bottom' | 'top' | Vertical alignment within the row. |
padding | number | string | — | Cell padding in pixels, or any CSS padding shorthand. |
stack | boolean | false | Become a full-width block below the breakpoint, so a row reflows into a stack. |
className | string | — | Extra class, for a rule of your own in a Style block. |
style | Style | — |
Alignment and stacking
Set stack on each column that should become full-width on a phone — never on the Row. A button inside follows its column, so a right-aligned cell puts the button on the right.
<Row>
<Column width="50%" padding={8} stack>
<Text>Stacks below 600px</Text>
</Column>
<Column width="50%" padding={8} align="right" stack>
<Button href="https://example.com">Open</Button>
</Column>
</Row>
Spacer
Vertical space.
| Prop | Type | Default | Notes |
|---|---|---|---|
height (required) | number | — | Height in pixels. |
Vertical space that survives Outlook
A margin would be simpler and is not dependable — Outlook collapses and ignores margins in several positions. This is a row with a stated height and a pinned line box.
<Section>
<Card padding={16}>
<Text>Above</Text>
</Card>
<Spacer height={24} />
<Card padding={16}>
<Text>Below</Text>
</Card>
</Section>
Hr
A rule.
| Prop | Type | Default | Notes |
|---|---|---|---|
style | Style | — | |
spacing | number | 0 | Space above and below, in pixels. |
A rule
Drawn as a bordered cell, not <hr> — Outlook gives <hr> its own inset 3D border and ignores most styling on it.
<Section>
<Text>Order summary</Text>
<Hr spacing={16} />
<Text variant="muted">Subtotal, tax and total follow.</Text>
</Section>
Typography
Heading
A section heading.
| Prop | Type | Default | Notes |
|---|---|---|---|
children | ReactNode | — | |
level | HeadingLevel | 1 | Semantic level. Drives the element, and the default size unless size overrides it. |
size | string | — | Font size, overriding the one level implies. A CSS length — rem is unsupported in two floor clients, so use px. |
align | 'left' | 'center' | 'right' | — | |
style | Style | — |
Levels
The element follows level; size overrides the size alone, for the common case of an <h2> that has to look like an <h1>.
<Section>
<Heading level={1}>Level one</Heading>
<Spacer height={8} />
<Heading level={2}>Level two</Heading>
<Spacer height={8} />
<Heading level={3}>Level three</Heading>
</Section>
Text
A paragraph.
| Prop | Type | Default | Notes |
|---|---|---|---|
children | ReactNode | — | |
size | string | '16px' | Font size as a CSS length. Never below 14px: iOS Mail inflates smaller text to its own minimum and the layout shifts under it. |
variant | 'default' | 'muted' | 'default' | muted for secondary copy — reads from --cascivo-color-text-muted. |
align | 'left' | 'center' | 'right' | — | |
className | string | — | Extra class, for a rule of your own in a Style block. |
style | Style | — |
Body copy
Never set a size below 14px: iOS Mail inflates smaller text to its own minimum and the layout shifts under it.
<Section>
<Text>Default body copy, 16px, at the theme foreground.</Text>
<Spacer height={12} />
<Text variant="muted">Muted secondary copy for the supporting line.</Text>
<Spacer height={12} />
<Text align="center" size="14px">Centred, and one step down.</Text>
</Section>
Link
An inline link.
| Prop | Type | Default | Notes |
|---|---|---|---|
children | ReactNode | — | |
href (required) | string | — | Absolute URL. A relative path resolves against the mail client's own host. |
ping | string | — | Click-beacon URLs, space-separated — the ping attribute. |
style | Style | — |
Inline link
Reads --cascivo-color-accent-text rather than the accent fill — four of the twelve themes pick an accent that fails contrast as type.
<Section>
<Text>
Trouble with the button?
<Link href="https://cascivo.com/docs/email">Open the page directly</Link>
.
</Text>
</Section>
List
A bulleted or numbered list.
| Prop | Type | Default | Notes |
|---|---|---|---|
items (required) | ReactNode[] | — | One entry per list item. |
ordered | boolean | false | Render <ol> rather than <ul>. |
style | Style | — |
Bulleted and numbered
Both margin and padding are stated because Outlook mis-indents a <ul> without them.
<Section>
<List items={['Unlimited seats', 'CSV export', 'Audit log']} />
<Spacer height={16} />
<List ordered items={['Open the link', 'Choose a password', 'Sign in']} />
</Section>
Footer
The closing block — smaller, muted, centred.
| Prop | Type | Default | Notes |
|---|---|---|---|
children | ReactNode | — | |
style | Style | — |
The closing block
Smaller, muted and centred, with the padding on a cell. The unsubscribe line belongs here.
<Footer>
<Text size="14px" variant="muted" align="center">Acme, Inc · Berlin</Text>
<Text size="14px" variant="muted" align="center">
<Link href="https://example.com/unsubscribe">Unsubscribe</Link>
</Text>
</Footer>
Content
Button
A call-to-action button.
| Prop | Type | Default | Notes |
|---|---|---|---|
children | ReactNode | — | |
href (required) | string | — | Absolute URL. A relative path resolves against the mail client's own host. |
variant | 'primary' | 'secondary' | 'destructive' | 'primary' | primary for the main action, secondary for a supporting one, destructive for anything that deletes. |
align | 'left' | 'center' | 'right' | — | Horizontal placement. Omit it to follow the cell the button sits in. |
block | boolean | false | Full-width call to action. |
ping | string | — | Click-beacon URLs, space-separated — the ping attribute. Same caveats as Link's. |
style | Style | — |
Variants
An <a> painted as a block inside its own table — a <button> does nothing in an email and several clients strip it. The corners are square in Outlook Windows, which is the one deliberate degradation.
<Section>
<Button href="https://example.com">Primary</Button>
<Spacer height={12} />
<Button href="https://example.com" variant="secondary">Secondary</Button>
<Spacer height={12} />
<Button href="https://example.com" variant="destructive">
Destructive
</Button>
</Section>
Full width, and following its cell
Omit align and the button follows the align of the Column or Section around it. Set it only inside a cell you aligned by hand with style.
<Section>
<Button href="https://example.com" block>Confirm your address</Button>
<Spacer height={12} />
<Section align="center">
<Button href="https://example.com" variant="secondary">
Centred by its cell
</Button>
</Section>
</Section>
Card
A bordered surface.
| Prop | Type | Default | Notes |
|---|---|---|---|
children | ReactNode | — | |
padding | number | 24 | Inset in pixels, applied to the cell — padding on a <div> is dropped in Outlook Windows. |
className | string | — | Extra class, for a rule of your own in a Style block. |
style | Style | — |
A bordered surface
One table, one cell, border and padding both on the cell — the only arrangement Outlook renders with the border in the right place.
<Card padding={24}>
<Heading level={3}>Pro plan</Heading>
<Spacer height={8} />
<Text variant="muted">€29 per seat, per month. Cancel any time.</Text>
<Spacer height={16} />
<Button href="https://example.com">Upgrade</Button>
</Card>
Alert
A callout.
| Prop | Type | Default | Notes |
|---|---|---|---|
children | ReactNode | — | |
tone | 'info' | 'success' | 'warning' | 'destructive' | 'info' | Drawn as a border and a tint, never an icon — images are blocked by default in a large share of clients. |
title | string | — | Optional bold lead-in above the body copy. |
style | Style | — |
Tones
The tone is a 4px border plus a tint, never an icon — icons would have to be images, and images are blocked by default in a large share of clients, which would hide the tone exactly where it matters.
<Section>
<Alert tone="info" title="Heads up">Your trial ends on Friday.</Alert>
<Spacer height={12} />
<Alert tone="success">Payment received.</Alert>
<Spacer height={12} />
<Alert tone="warning">Your card expires next month.</Alert>
<Spacer height={12} />
<Alert tone="destructive">We could not charge your card.</Alert>
</Section>
Badge
A small status pill.
| Prop | Type | Default | Notes |
|---|---|---|---|
children | ReactNode | — | |
tone | 'neutral' | 'success' | 'warning' | 'destructive' | 'info' | 'neutral' | Carried by colour alone, so pair it with a label that says the same thing. |
style | Style | — |
Tones
The one primitive knowingly imperfect in Outlook Windows: a badge has to be inline, and inline padding is only partially supported there. The spaces either side are the fallback.
<Section>
<Text>
<Badge tone="success">Paid</Badge>
{' '}
<Badge tone="warning">Pending</Badge>
{' '}
<Badge tone="destructive">Failed</Badge>
{' '}
<Badge tone="info">New</Badge>
{' '}
<Badge>Draft</Badge>
</Text>
</Section>
Img
A raster image.
| Prop | Type | Default | Notes |
|---|---|---|---|
src (required) | string | — | Absolute URL. A relative path resolves against the client's own host and 404s. |
alt (required) | string | — | Required. An image with no alt text is unreadable in the ~40% of clients that block images by default. |
width (required) | number | — | Intrinsic display width in pixels. Required: Outlook sizes from the attribute and renders at intrinsic size without it. |
height | number | — | Fixed height, in pixels, emitted as the height attribute only. |
href | string | — | Wrap the image in a link. |
style | Style | — |
A raster image
src must be an absolute URL — a relative path resolves against the client’s own host. alt and width are required by the type: images are blocked by default in a large share of clients, and Outlook renders at intrinsic size without the attribute. SVG is not usable.
<Section align="center">
<Img
src="https://cascivo.com/icon-192.png"
alt="cascivo"
width={96}
href="https://cascivo.com"
/>
</Section>
Markdown
Render a Markdown string through the cascivo email primitives.
| Prop | Type | Default | Notes |
|---|---|---|---|
children (required) | string | — | The Markdown source. |
spacing | number | 16 | Vertical space between blocks, in pixels. |
imageWidth | number | CONTENT_WIDTH | Width in pixels for  images. |
Prose you do not have at build time
Every node renders through the primitives above, never through raw HTML — so a CMS string is exactly as conformance-checkable as a hand-composed tree. Anything outside the allowlist stays literal text rather than failing the send.
<Markdown>
{`## Changes this week
We shipped **two** things you asked for:
- Per-seat billing, prorated
- CSV export on every report
Read the [full notes](https://cascivo.com/docs/changelog).`}
</Markdown>As Markdown: /docs/email-primitives.md