Email layout

Pro

The shell every transactional email uses: brand, title, body, one next step (a command to run, or a button), the ids it refers to, and why you got it. Table layout and inline styles so it survives email clients; render it to an HTML string with renderEmail.

Markdown

Preview

Install

With the Candor CLI

First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/email-layout

Usage

TSX · The example above
"use client"

import * as React from "react"
import { EmailPreview, renderEmail, type EmailNotice } from "@/components/email-layout"
import { Button } from "@/components/ui/button"

const notice: EmailNotice = {
  preheader: "Add region to customers is waiting for an approver on Acme web Production.",
  title: "An approval is waiting for you",
  paragraphs: ["Keel asked for approval of a migration to Acme web Production. You’re one of the people who can decide it.", "It expires in 7 days."],
  next: { kind: "command", text: "keel approval show apr_7f3a91" },
  refs: [
    { label: "Request", value: "apr_7f3a91" },
    { label: "Change hash", value: "sha256:9f2c4e81b7a0…" },
  ],
  why: "You’re getting this because you’re an approver on Acme web Production.",
}

export default function Example() {
  const [frame, setFrame] = React.useState(false)
  // renderEmail's string goes to your mailer. EmailPreview shows the same email inline; the iframe shows the exact document.
  const html = React.useMemo(() => renderEmail(notice, "Keel"), [])
  return (
    <div className="flex max-w-xl flex-col gap-3">
      <EmailPreview notice={notice} brand="Keel" className="border" />
      <Button size="sm" variant="outline" className="self-start" aria-expanded={frame} onClick={() => setFrame((o) => !o)}>
        {frame ? "Hide the HTML document" : "Show the HTML document"}
      </Button>
      {frame && <iframe title="Email document" srcDoc={html} sandbox="" className="block h-[30rem] w-full rounded-xl border bg-white" />}
    </div>
  )
}

<EmailLayout>

PropTypeDefaultDescription
noticerequiredEmailNotice—What the email says: preheader, title, body, next step, ids and why it was sent.
brandrequiredstring—The product name shown at the top.
colorsEmailColors | undefineddefaultEmailColorsFixed hex colours to match your brand. Defaults to the default theme's light values.
stringsPartial<typeof emailLayoutStrings> | undefined—The email's fixed words, e.g. `{ nextRun: "Ensuite, lancez :" }`. Passed directly: emails render outside any LocaleProvider.
langstring | undefined"en"The email's language, for `<html lang>`.

<EmailPreview>

The same email inline, for a settings page or the docs: no <html> document or iframe, its own fixed colours so it looks as it will in a mail client, and its title at a heading level that fits your page.

PropTypeDefaultDescription
noticerequiredEmailNotice—What the email says.
brandrequiredstring—The product name shown at the top.
colorsEmailColors | undefineddefaultEmailColorsFixed hex colours; defaults to the default theme's light values.
stringsPartial<typeof emailLayoutStrings> | undefined—The email's fixed words.
headingLevel2 | 3 | 4 | undefined3The heading level of the email's title on your page. Default 3.
labelstring | undefined—The preview's accessible name. Default: "Email preview: " and the title.
classNamestring | undefined—Extra classes for the outer element.

Functions

renderEmail(notice: EmailNotice, brand: string, colors?: EmailColors, words?: { strings?: Partial<typeof emailLayoutStrings> | undefined; lang?: string | undefined })

The email as a complete HTML document, ready for the mailer.

Strings

The fixed words, in English by default. Change any of them under emailLayout in <LocaleProvider strings={{ … }}>; the table is exported as emailLayoutStrings.

KeyEnglish
nextRunNext, run:
previewEmail preview

Types

EmailColors
{ ground: string; card: string; ink: string; muted: string; border: string; accent: string; code: string }
EmailNextStep
| { kind: "command"; text: string } | { kind: "link"; label: string; href: string }
EmailNotice
{ /** Shown in the inbox preview. */ preheader: string title: string paragraphs: string[] next?: EmailNextStep | undefined /** Ids the notice carries, e.g. a request id and a hash, rather than the data itself. */ refs?: { label: string; value: string }[] | undefined /** Why the person got this, e.g. "You can approve changes to Shop production." */ why: string }