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

The root element.

PropTypeDefaultNotes
childrenReactNode—
langstring'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>

Document head.

PropTypeDefaultNotes
childrenReactNode—
titlestring—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.

PropTypeDefaultNotes
childrenReactNode—
styleStyle—
langstring'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.

PropTypeDefaultNotes
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.

PropTypeDefaultNotes
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.

PropTypeDefaultNotes
childrenReactNode—
widthnumberCONTENT_WIDTHContent width in pixels.
responsivebooleantrueEmit the width override that lets the container go fluid on a phone.
breakpointnumber—Viewport width, in pixels, below which the container goes fluid.
classNamestring—Extra class, for a rule of your own in a Style block.
styleStyle—

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.

PropTypeDefaultNotes
childrenReactNode—
paddingnumber | string0Vertical padding in pixels, applied to the cell.
backgroundstring—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.
classNamestring—Extra class, for a rule of your own in a Style block.
styleStyle—

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.

PropTypeDefaultNotes
childrenReactNode—
classNamestring—Extra class, for a rule of your own in a Style block.
styleStyle—

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.

PropTypeDefaultNotes
childrenReactNode—
widthnumber | 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.
paddingnumber | string—Cell padding in pixels, or any CSS padding shorthand.
stackbooleanfalseBecome a full-width block below the breakpoint, so a row reflows into a stack.
classNamestring—Extra class, for a rule of your own in a Style block.
styleStyle—

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.

PropTypeDefaultNotes
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.

PropTypeDefaultNotes
styleStyle—
spacingnumber0Space 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.

PropTypeDefaultNotes
childrenReactNode—
levelHeadingLevel1Semantic level. Drives the element, and the default size unless size overrides it.
sizestring—Font size, overriding the one level implies. A CSS length — rem is unsupported in two floor clients, so use px.
align'left' | 'center' | 'right'—
styleStyle—

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.

PropTypeDefaultNotes
childrenReactNode—
sizestring'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'—
classNamestring—Extra class, for a rule of your own in a Style block.
styleStyle—

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>

An inline link.

PropTypeDefaultNotes
childrenReactNode—
href (required)string—Absolute URL. A relative path resolves against the mail client's own host.
pingstring—Click-beacon URLs, space-separated — the ping attribute.
styleStyle—

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.

PropTypeDefaultNotes
items (required)ReactNode[]—One entry per list item.
orderedbooleanfalseRender <ol> rather than <ul>.
styleStyle—

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>

The closing block — smaller, muted, centred.

PropTypeDefaultNotes
childrenReactNode—
styleStyle—

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.

PropTypeDefaultNotes
childrenReactNode—
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.
blockbooleanfalseFull-width call to action.
pingstring—Click-beacon URLs, space-separated — the ping attribute. Same caveats as Link's.
styleStyle—

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.

PropTypeDefaultNotes
childrenReactNode—
paddingnumber24Inset in pixels, applied to the cell — padding on a <div> is dropped in Outlook Windows.
classNamestring—Extra class, for a rule of your own in a Style block.
styleStyle—

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.

PropTypeDefaultNotes
childrenReactNode—
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.
titlestring—Optional bold lead-in above the body copy.
styleStyle—

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.

PropTypeDefaultNotes
childrenReactNode—
tone'neutral' | 'success' | 'warning' | 'destructive' | 'info''neutral'Carried by colour alone, so pair it with a label that says the same thing.
styleStyle—

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.

PropTypeDefaultNotes
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.
heightnumber—Fixed height, in pixels, emitted as the height attribute only.
hrefstring—Wrap the image in a link.
styleStyle—

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.

PropTypeDefaultNotes
children (required)string—The Markdown source.
spacingnumber16Vertical space between blocks, in pixels.
imageWidthnumberCONTENT_WIDTHWidth in pixels for ![alt](https://…) 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

← All guides