diff --git a/.changeset/config.json b/.changeset/config.json
index 46fa368d8c..f799187208 100644
--- a/.changeset/config.json
+++ b/.changeset/config.json
@@ -1,16 +1,17 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
- "changelog": "@changesets/cli/changelog",
- "commit": false,
- "fixed": [["react-email-starter", "create-email"]],
- "linked": [],
"access": "public",
"baseBranch": "main",
- "updateInternalDependencies": "patch",
+ "changelog": "@changesets/cli/changelog",
+ "commit": false,
+ "fixed": [["react-email", "@react-email/ui"]],
"ignore": [
- "@benchmarks/preview-server",
+ "@benchmarks/ui",
"@benchmarks/tailwind-component",
+ "playground",
"demo",
+ "email-dev",
"web"
- ]
+ ],
+ "updateInternalDependencies": "patch"
}
diff --git a/benchmarks/tailwind-component/tailwind.config.js b/.github/CODEOWNERS
similarity index 100%
rename from benchmarks/tailwind-component/tailwind.config.js
rename to .github/CODEOWNERS
diff --git a/.github/ISSUE_TEMPLATE/1.bug_report.yml b/.github/ISSUE_TEMPLATE/1.bug_report.yml
index 8765ea8d2b..db68fc94f5 100644
--- a/.github/ISSUE_TEMPLATE/1.bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/1.bug_report.yml
@@ -2,6 +2,10 @@ name: Bug Report
description: Create a bug report for React Email
labels: ["Type: Bug"]
body:
+ - type: input
+ attributes:
+ label: What versions are you using? (if relevant)
+ value: "@react-email/components@x.y.z, react-email@x.y.z, etc."
- type: textarea
attributes:
label: Describe the Bug
@@ -10,45 +14,47 @@ body:
required: true
- type: dropdown
attributes:
- label: Which package is affected (leave empty if unsure)
+ label: What is affected (leave empty if unsure)
multiple: true
options:
- - "@react-email/body"
- - "@react-email/button"
- - "@react-email/column"
- - "@react-email/components"
- - "@react-email/container"
- - "@react-email/font"
- - "@react-email/head"
- - "@react-email/heading"
- - "@react-email/hr"
- - "@react-email/html"
- - "@react-email/img"
- - "@react-email/link"
- - "@react-email/preview"
- - "@react-email/render"
- - "@react-email/row"
- - "@react-email/section"
- - "@react-email/tailwind"
- - "@react-email/text"
- - "client"
- - "create-email"
- - "demo"
- - "docs"
- - "examples"
- - "react-email"
- - "web"
- - type: input
+ - "Preview Server"
+ - "CLI"
+ - "Html Component"
+ - "Body Component"
+ - "Head Component"
+ - "Button Component"
+ - "Container Component"
+ - "CodeBlock Component"
+ - "CodeInline Component"
+ - "Column Component"
+ - "Row Component"
+ - "Font Component"
+ - "Heading Component"
+ - "Hr Component"
+ - "Img Component"
+ - "Link Component"
+ - "Markdown Component"
+ - "Preview Component"
+ - "Section Component"
+ - "Tailwind Component"
+ - "Text Component"
+ - "Render Utility"
+ - "@react-email/components package"
+ - "npx create-email"
+ - "Demo"
+ - "Website"
+ - "Examples"
+ - type: textarea
attributes:
label: Link to the code that reproduces this issue
- description: |
- A link to a GitHub repository minimal reproduction. A minimal reproduction code is really helpful to understand the issue.
+ value: |
+ A link to a GitHub repository minimal reproduction. Not your entire project, just the code necessary to reproduce the issue. Try going from the starter `npx create-email@latest` and adding only what's needed to cause the issue. If you don't share a reproduction, we might close the issue or it will take significantly longer for things to get sorted out.
validations:
required: true
- type: textarea
attributes:
label: To Reproduce
- description: Steps to reproduce the behavior, please provide a clear description of how to reproduce the issue, based on the linked minimal reproduction. Screenshots can be provided in the issue body below. If using code blocks, make sure that [syntax highlighting is correct](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#syntax-highlighting) and double check that the rendered preview is not broken.
+ value: Steps to reproduce the behavior, please provide a clear description of how to reproduce the issue, based on the linked minimal reproduction. Screenshots can be provided in the issue body below. If using code blocks, make sure that [syntax highlighting is correct](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#syntax-highlighting) and double check that the rendered preview is not broken.
validations:
required: true
- type: textarea
@@ -67,3 +73,4 @@ body:
attributes:
label: What's your node version? (if relevant)
description: "Please specify the exact version."
+
diff --git a/.github/workflows/bump-canary.yml b/.github/workflows/bump-canary.yml
deleted file mode 100644
index 93b064cd2c..0000000000
--- a/.github/workflows/bump-canary.yml
+++ /dev/null
@@ -1,52 +0,0 @@
-name: Bump canary
-
-on:
- push:
- branches:
- - canary
-
-concurrency: ${{ github.workflow }}-${{ github.ref }}
-
-jobs:
- release:
- name: Bump for canary
- runs-on: ubuntu-latest
- steps:
- - name: Checkout Repo
- uses: actions/checkout@v4
-
- - name: Setup Node.js 22
- uses: actions/setup-node@v4
- with:
- node-version: 22
-
- - name: Enable Corepack
- id: pnpm-setup
- run: |
- corepack enable
- corepack prepare pnpm@9.15.0 --activate
- pnpm config set script-shell "/usr/bin/bash"
- echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
-
- - name: pnpm Cache
- uses: buildjet/cache@v4
- with:
- path: ${{ steps.pnpm-setup.outputs.pnpm_cache_dir }}
- key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- restore-keys: |
- ${{ runner.os }}-pnpm-store-
-
- - name: Install packages
- run: pnpm install --frozen-lockfile
-
- - name: Enter prerelease mode
- continue-on-error: true
- run: pnpm canary:enter
-
- - name: Create Release Pull Request
- uses: changesets/action@v1
- with:
- version: pnpm run version
- title: "chore: Bump for release"
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/bump-stable.yml b/.github/workflows/bump-stable.yml
deleted file mode 100644
index d32139032a..0000000000
--- a/.github/workflows/bump-stable.yml
+++ /dev/null
@@ -1,52 +0,0 @@
-name: Bump
-
-on:
- push:
- branches:
- - main
-
-concurrency: ${{ github.workflow }}-${{ github.ref }}
-
-jobs:
- release:
- name: Bump
- runs-on: ubuntu-latest
- steps:
- - name: Checkout Repo
- uses: actions/checkout@v4
-
- - name: Setup Node.js 22
- uses: actions/setup-node@v4
- with:
- node-version: 22
-
- - name: Enable Corepack
- id: pnpm-setup
- run: |
- corepack enable
- corepack prepare pnpm@9.15.0 --activate
- pnpm config set script-shell "/usr/bin/bash"
- echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
-
- - name: pnpm Cache
- uses: buildjet/cache@v4
- with:
- path: ${{ steps.pnpm-setup.outputs.pnpm_cache_dir }}
- key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- restore-keys: |
- ${{ runner.os }}-pnpm-store-
-
- - name: Install packages
- run: pnpm install --frozen-lockfile
-
- - name: Exit prerelease mode
- continue-on-error: true
- run: pnpm canary:exit
-
- - name: Create Release Pull Request
- uses: changesets/action@v1
- with:
- version: pnpm run version
- title: "chore: Bump for release"
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/bump.yml b/.github/workflows/bump.yml
new file mode 100644
index 0000000000..f89cb8dd65
--- /dev/null
+++ b/.github/workflows/bump.yml
@@ -0,0 +1,47 @@
+name: Bump
+on:
+ push:
+ branches:
+ - canary
+concurrency: ${{ github.workflow }}-${{ github.ref }}
+jobs:
+ bump:
+ timeout-minutes: 30
+ runs-on: depot-ubuntu-22.04-2
+ permissions:
+ contents: write
+ pull-requests: write
+ container:
+ image: node:24
+ steps:
+ - name: Generate GitHub App token
+ id: app-token
+ uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349
+ with:
+ app-id: ${{ secrets.BOT_APP_ID }}
+ private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }}
+ permission-contents: write
+ permission-pull-requests: write
+ - name: Checkout Repo
+ uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98
+ with:
+ fetch-depth: 1
+ token: ${{ steps.app-token.outputs.token }}
+ - run: git config --global --add safe.directory $GITHUB_WORKSPACE
+ - name: pnpm setup
+ uses: pnpm/action-setup@738f428026a1f5a72398de22aeed83d859c4a660
+ - name: Install packages
+ run: pnpm install --frozen-lockfile --prefer-offline
+ - name: Configure version bump git user
+ run: |
+ git config user.name "resend-version-bump[bot]"
+ git config user.email "277115511+resend-version-bump[bot]@users.noreply.github.com"
+ - name: Create "version packages" pull request
+ uses: changesets/action@6a0a831ff30acef54f2c6aa1cbbc1096b066edaf
+ with:
+ version: pnpm run version
+ title: "chore(root): version packages"
+ commitMode: git-cli
+ setupGitUser: false
+ env:
+ GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
new file mode 100644
index 0000000000..a1db0ddd7c
--- /dev/null
+++ b/.github/workflows/e2e.yml
@@ -0,0 +1,52 @@
+name: E2E Tests
+on:
+ push:
+ branches:
+ - main
+ - canary
+ pull_request:
+permissions:
+ contents: read
+ pull-requests: read
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+jobs:
+ e2e:
+ # Org secrets in step env: push + same-repo PRs only; fork PRs evaluate to empty (explicit for review).
+ timeout-minutes: 45
+ runs-on: depot-ubuntu-22.04-8
+ container:
+ image: mcr.microsoft.com/playwright:v1.59.1-noble
+ steps:
+ - name: Checkout Repo
+ uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98
+ with:
+ fetch-depth: 1
+ - run: git config --global --add safe.directory $GITHUB_WORKSPACE
+ - name: Setup Node.js
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
+ with:
+ node-version: 24
+ - name: pnpm setup
+ uses: pnpm/action-setup@738f428026a1f5a72398de22aeed83d859c4a660
+ - name: Install packages
+ run: pnpm install --frozen-lockfile --prefer-offline
+ - name: Install Playwright browsers
+ run: pnpm exec playwright install chromium --with-deps
+ - name: Run Build
+ run: pnpm build
+ env:
+ REDIS_URL: redis://localhost:6379
+ SPAM_ASSASSIN_HOST: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && secrets.SPAM_ASSASSIN_HOST || '' }}
+ SPAM_ASSASSIN_PORT: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && secrets.SPAM_ASSASSIN_PORT || '' }}
+ TURBO_TOKEN: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && secrets.TURBO_TOKEN || '' }}
+ TURBO_TEAM: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && secrets.TURBO_TEAM || '' }}
+ - name: Run Tailwind integration tests
+ run: pnpm turbo test:e2e
+ env:
+ REDIS_URL: redis://localhost:6379
+ SPAM_ASSASSIN_HOST: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && secrets.SPAM_ASSASSIN_HOST || '' }}
+ SPAM_ASSASSIN_PORT: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && secrets.SPAM_ASSASSIN_PORT || '' }}
+ TURBO_TOKEN: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && secrets.TURBO_TOKEN || '' }}
+ TURBO_TEAM: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && secrets.TURBO_TEAM || '' }}
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 0000000000..67611c78f1
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,32 @@
+name: Lint
+on:
+ push:
+ branches:
+ - main
+ - canary
+ pull_request:
+permissions:
+ contents: read
+ pull-requests: read
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+jobs:
+ lint:
+ timeout-minutes: 20
+ runs-on: depot-ubuntu-22.04-2
+ container:
+ image: node:24-slim
+ steps:
+ - name: Checkout Repo
+ uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98
+ with:
+ fetch-depth: 1
+ - name: pnpm setup
+ uses: pnpm/action-setup@738f428026a1f5a72398de22aeed83d859c4a660
+ - name: Install packages
+ run: pnpm install --frozen-lockfile --prefer-offline
+ - name: Run Lint
+ run: pnpm lint
+ env:
+ SKIP_ENV_VALIDATION: true
diff --git a/.github/workflows/pin-dependencies-check.yml b/.github/workflows/pin-dependencies-check.yml
new file mode 100644
index 0000000000..9db1a8068c
--- /dev/null
+++ b/.github/workflows/pin-dependencies-check.yml
@@ -0,0 +1,30 @@
+name: Pin Dependencies Check
+on:
+ push:
+ branches:
+ - main
+ - canary
+ pull_request:
+permissions:
+ contents: read
+ pull-requests: read
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+jobs:
+ pin-dependencies-check:
+ timeout-minutes: 15
+ runs-on: depot-ubuntu-22.04-2
+ container:
+ image: node:24-slim
+ steps:
+ - name: Checkout Repo
+ uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98
+ with:
+ fetch-depth: 1
+ - name: pnpm setup
+ uses: pnpm/action-setup@738f428026a1f5a72398de22aeed83d859c4a660
+ - name: Install root dependencies only
+ run: pnpm install --frozen-lockfile --filter . --ignore-scripts
+ - name: Check for pinned dependencies
+ run: pnpm exec tsx ./scripts/check-dependency-versions.ts
diff --git a/.github/workflows/preview-release.yml b/.github/workflows/preview-release.yml
new file mode 100644
index 0000000000..26f84ff80c
--- /dev/null
+++ b/.github/workflows/preview-release.yml
@@ -0,0 +1,45 @@
+name: Preview Release
+on:
+ pull_request:
+permissions:
+ contents: read
+ pull-requests: write
+concurrency: ${{ github.workflow }}-${{ github.ref }}
+jobs:
+ preview-release:
+ # Build secrets: same-repo PRs only; fork PRs evaluate to empty (explicit for review).
+ timeout-minutes: 45
+ runs-on: depot-ubuntu-22.04-8
+ permissions:
+ contents: write
+ pull-requests: write
+ container:
+ image: node:24
+ steps:
+ - name: Checkout Repo
+ uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98
+ with:
+ fetch-depth: 2
+ - run: git config --global --add safe.directory $GITHUB_WORKSPACE
+ - name: pnpm setup
+ uses: pnpm/action-setup@738f428026a1f5a72398de22aeed83d859c4a660
+ - name: Install packages
+ run: pnpm install --frozen-lockfile --prefer-offline
+ - name: Find changed packages
+ id: changed_packages
+ uses: tj-actions/changed-files@3c4bc6fa0ca4718d438e0a4bd3ea81fbb0e6e2be
+ with:
+ files: packages/**
+ dir_names: true
+ dir_names_max_depth: 2
+ - name: Run Build
+ if: steps.changed_packages.outputs.all_changed_and_modified_files != ''
+ run: pnpm build
+ env:
+ SPAM_ASSASSIN_HOST: ${{ github.event.pull_request.head.repo.full_name == github.repository && secrets.SPAM_ASSASSIN_HOST || '' }}
+ SPAM_ASSASSIN_PORT: ${{ github.event.pull_request.head.repo.full_name == github.repository && secrets.SPAM_ASSASSIN_PORT || '' }}
+ TURBO_TOKEN: ${{ github.event.pull_request.head.repo.full_name == github.repository && secrets.TURBO_TOKEN || '' }}
+ TURBO_TEAM: ${{ github.event.pull_request.head.repo.full_name == github.repository && secrets.TURBO_TEAM || '' }}
+ - name: Publish changed packages to pkg.pr.new
+ if: steps.changed_packages.outputs.all_changed_and_modified_files != ''
+ run: pnpm pkg-pr-new publish ${{ steps.changed_packages.outputs.all_changed_and_modified_files }} --pnpm
diff --git a/.github/workflows/pull-request-title-check.yml b/.github/workflows/pull-request-title-check.yml
new file mode 100644
index 0000000000..2c86103283
--- /dev/null
+++ b/.github/workflows/pull-request-title-check.yml
@@ -0,0 +1,26 @@
+name: Pull Request Title Check
+on:
+ pull_request:
+ types: [opened, edited, synchronize]
+permissions:
+ pull-requests: read
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+jobs:
+ pull-request-title-check:
+ timeout-minutes: 15
+ runs-on: depot-ubuntu-22.04-2
+ container:
+ image: node:24-slim
+ steps:
+ - name: Checkout Repo
+ uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98
+ with:
+ fetch-depth: 1
+ - name: pnpm setup
+ uses: pnpm/action-setup@738f428026a1f5a72398de22aeed83d859c4a660
+ - name: Install packages
+ run: pnpm install --frozen-lockfile --prefer-offline
+ - name: Check pull request title
+ run: pnpm tsx ./scripts/pull-request-title-check.ts
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000000..d33e94b5db
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,33 @@
+name: Release
+on:
+ push:
+ branches:
+ - main
+ - canary
+permissions:
+ contents: read
+concurrency: ${{ github.workflow }}-${{ github.ref }}
+jobs:
+ release:
+ # npm trusted publishing (OIDC) is only supported on GitHub-hosted `ubuntu-latest` runners.
+ runs-on: ubuntu-latest
+ timeout-minutes: 45
+ permissions:
+ id-token: write
+ contents: write
+ container:
+ image: node:24
+ steps:
+ - name: Checkout Repo
+ uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98
+ with:
+ fetch-depth: 1
+ - run: git config --global --add safe.directory $GITHUB_WORKSPACE
+ - name: pnpm setup
+ uses: pnpm/action-setup@738f428026a1f5a72398de22aeed83d859c4a660
+ - name: Install packages
+ run: pnpm install --frozen-lockfile --prefer-offline
+ - name: Publish release
+ run: node --import tsx/esm ./scripts/release.mts
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/sync-prs-to-linear.yml b/.github/workflows/sync-prs-to-linear.yml
new file mode 100644
index 0000000000..7611962a0f
--- /dev/null
+++ b/.github/workflows/sync-prs-to-linear.yml
@@ -0,0 +1,24 @@
+name: Sync Open PRs to Linear
+
+on:
+ schedule:
+ - cron: '0 10 * * *'
+ workflow_dispatch:
+
+permissions:
+ contents: none
+
+jobs:
+ sync:
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ permissions:
+ pull-requests: write
+ issues: write
+
+ steps:
+ - uses: resend/public-shared-workflows/.github/actions/sync_prs_to_linear@6adbbae7541681ead204faab5d4e5ea9bebca4da
+ with:
+ linear-api-key: ${{ secrets.LINEAR_API_KEY }}
+ linear-team-id: ${{ secrets.LINEAR_TEAM_ID }}
+ github-token: ${{ github.token }}
diff --git a/.github/workflows/sync-skills.yml b/.github/workflows/sync-skills.yml
new file mode 100644
index 0000000000..cdcaf098e1
--- /dev/null
+++ b/.github/workflows/sync-skills.yml
@@ -0,0 +1,27 @@
+name: Sync Skills
+
+on:
+ push:
+ branches: [main]
+ paths: ['skills/**']
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ sync:
+ timeout-minutes: 10
+ runs-on: depot-ubuntu-22.04-2
+ steps:
+ - name: Trigger sync on resend-skills
+ env:
+ GH_TOKEN: ${{ secrets.SYNC_SKILLS_TO_RESEND_SKILLS }}
+ run: |
+ gh workflow run sync-from-repo.yml \
+ --repo resend/resend-skills \
+ --field repo=react-email \
+ --field skill-path=skills/react-email \
+ --field skill-name=react-email \
+ --field sha=${{ github.sha }} \
+ --field reviewer=${{ github.actor }}
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 496550d323..4ebbf028e9 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -1,150 +1,51 @@
-name: rsnd
+name: Tests
on:
push:
branches:
- main
+ - canary
pull_request:
+permissions:
+ contents: read
+ pull-requests: read
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
jobs:
- lint:
- runs-on: buildjet-4vcpu-ubuntu-2204
+ tests:
+ # Org secrets in step env: push + same-repo PRs only; fork PRs evaluate to empty (explicit for review).
+ timeout-minutes: 45
+ runs-on: depot-ubuntu-22.04-8
container:
- image: node:22
+ image: mcr.microsoft.com/playwright:v1.59.1-noble
steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Enable Corepack
- id: pnpm-setup
- run: |
- corepack enable
- corepack prepare pnpm@9.15.0 --activate
- pnpm config set script-shell "/usr/bin/bash"
- echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
-
- - name: pnpm Cache
- uses: buildjet/cache@v4
+ - name: Checkout Repo
+ uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98
with:
- path: ${{ steps.pnpm-setup.outputs.pnpm_cache_dir }}
- key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- restore-keys: |
- ${{ runner.os }}-pnpm-store-
-
- - name: Install packages
- run: pnpm install --frozen-lockfile
-
- - name: Run Build
- run: pnpm build
-
- - name: Run Lint
- run: pnpm lint
-
- test:
- runs-on: buildjet-4vcpu-ubuntu-2204
- container:
- image: node:22
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Enable Corepack
- id: pnpm-setup
- run: |
- corepack enable
- corepack prepare pnpm@9.15.0 --activate
- pnpm config set script-shell "/usr/bin/bash"
- echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
-
- - name: pnpm Cache
- uses: buildjet/cache@v4
+ fetch-depth: 1
+ - run: git config --global --add safe.directory $GITHUB_WORKSPACE
+ - name: Setup Node.js
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
with:
- path: ${{ steps.pnpm-setup.outputs.pnpm_cache_dir }}
- key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- restore-keys: |
- ${{ runner.os }}-pnpm-store-
-
+ node-version: 24
+ - name: pnpm setup
+ uses: pnpm/action-setup@738f428026a1f5a72398de22aeed83d859c4a660
- name: Install packages
- run: pnpm install --frozen-lockfile
-
+ run: pnpm install --frozen-lockfile --prefer-offline
- name: Run Build
run: pnpm build
-
+ env:
+ REDIS_URL: redis://localhost:6379
+ SPAM_ASSASSIN_HOST: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && secrets.SPAM_ASSASSIN_HOST || '' }}
+ SPAM_ASSASSIN_PORT: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && secrets.SPAM_ASSASSIN_PORT || '' }}
+ TURBO_TOKEN: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && secrets.TURBO_TOKEN || '' }}
+ TURBO_TEAM: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && secrets.TURBO_TEAM || '' }}
- name: Run Tests
run: pnpm test
- env:
- SPAM_ASSASSIN_HOST: ${{ secrets.SPAM_ASSASSIN_HOST }}
- SPAM_ASSASSIN_PORT: ${{ secrets.SPAM_ASSASSIN_PORT }}
-
- build:
- runs-on: buildjet-4vcpu-ubuntu-2204
- container:
- image: node:22
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Enable Corepack
- id: pnpm-setup
- run: |
- corepack enable
- corepack prepare pnpm@9.15.0 --activate
- pnpm config set script-shell "/usr/bin/bash"
- echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
-
- - name: pnpm Cache
- uses: buildjet/cache@v4
- with:
- path: ${{ steps.pnpm-setup.outputs.pnpm_cache_dir }}
- key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- restore-keys: |
- ${{ runner.os }}-pnpm-store-
-
- - name: Install packages
- run: pnpm install --frozen-lockfile
-
- - name: Run Build
- run: pnpm build
-
- dependencies:
- runs-on: buildjet-4vcpu-ubuntu-2204
- container:
- image: node:18
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Check for pinned dependencies
- run: |
- node -e '
- const fs = require("fs");
- const pkg = JSON.parse(fs.readFileSync("package.json", "utf8"));
- const errors = [];
-
- function isPinned(version) {
- if (version.startsWith("workspace:")) {
- return true;
- }
- if (version.startsWith("npm:")) {
- return true;
- }
- return /^\d+\.\d+\.\d+$|^[a-z]+:[a-z]+@\d+$/.test(version);
- }
-
- for (const [dep, version] of Object.entries(pkg.dependencies || {})) {
- if (!isPinned(version)) {
- errors.push(`Dependency "${dep}" is not pinned: "${version}"`);
- }
- }
-
- for (const [dep, version] of Object.entries(pkg.devDependencies || {})) {
- if (!isPinned(version)) {
- errors.push(`Dev dependency "${dep}" is not pinned: "${version}"`);
- }
- }
-
- if (errors.length > 0) {
- console.error(`\n${errors.join("\n")}\n`);
- process.exit(1);
- } else {
- console.log("All dependencies are pinned.");
- }
- '
+ env:
+ PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
+ REDIS_URL: redis://localhost:6379
+ SPAM_ASSASSIN_HOST: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && secrets.SPAM_ASSASSIN_HOST || '' }}
+ SPAM_ASSASSIN_PORT: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && secrets.SPAM_ASSASSIN_PORT || '' }}
+ TURBO_TOKEN: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && secrets.TURBO_TOKEN || '' }}
+ TURBO_TEAM: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && secrets.TURBO_TEAM || '' }}
diff --git a/.gitignore b/.gitignore
index d76b05d3b1..9c6d3b46be 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,11 +7,17 @@ node_modules
# testing
coverage
+**/.vitest-attachments/
+**/__screenshots__/
+**/*/package-lock.json
+**/*/yalc.lock
+.yalc*
# next.js
.next/
out/
build
+next-env.d.ts
dist
.vercel
@@ -33,5 +39,13 @@ yarn-error.log*
.env.test.local
.env.production.local
-# turbo
.turbo
+.worktrees
+.serena/
+
+# cursor
+.cursor/hooks/state/
+
+# mintlify exports (generated locally)
+apps/docs/export.zip
+apps/docs/export/
diff --git a/.npmrc b/.npmrc
deleted file mode 100644
index ded82e2f63..0000000000
--- a/.npmrc
+++ /dev/null
@@ -1 +0,0 @@
-auto-install-peers = true
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000000..7b46e2848f
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,7 @@
+{
+ "editor.defaultFormatter": "biomejs.biome",
+ "editor.formatOnSave": true,
+ "editor.codeActionsOnSave": {
+ "source.organizeImports.biome": "explicit"
+ }
+}
diff --git a/README.md b/README.md
index a68467d544..bf12000c07 100644
--- a/README.md
+++ b/README.md
@@ -4,11 +4,9 @@
The next generation of writing emails. High-quality, unstyled components for creating emails.
## Introduction
@@ -18,38 +16,22 @@ It reduces the pain of coding responsive emails with dark mode support. It also
## Why
-We believe that email is an extremely important medium for people to communicate. However, we need to stop developing emails like 2010, and rethink how email can be done in 2022 and beyond. Email development needs a revamp. A renovation. Modernized for the way we build web apps today.
+We believe that email is an extremely important medium for people to communicate. However, we need to stop developing emails like 2010, and rethink how email can be done in 2026 and beyond. Email development needs a revamp. A renovation. Modernized for the way we build web apps today.
## Install
-Install one of the components from your command line.
-
-#### With yarn
-
-```sh
-yarn add @react-email/components -E
-```
-
-#### With npm
-
-```sh
-npm install @react-email/components -E
-```
-
-#### With pnpm
-
```sh
-pnpm install @react-email/components -E
+npm i react-email@latest
```
## Getting started
-Add the component to your email template. Include styles where needed.
+Define your email template with React, include styles and our components where needed.
```jsx
-import { Button } from "@react-email/components";
+import { Button } from "react-email";
-const Email = () => {
+export default function Email() {
return (
Click me
@@ -62,23 +44,31 @@ const Email = () => {
A set of standard components to help you build amazing emails without having to deal with the mess of creating table-based layouts and maintaining archaic markup.
-- [Body](https://github.com/resend/react-email/tree/main/packages/body)
-- [Button](https://github.com/resend/react-email/tree/main/packages/button)
-- [CodeBlock](https://github.com/resend/react-email/tree/main/packages/code-block)
-- [CodeInline](https://github.com/resend/react-email/tree/main/packages/code-inline)
-- [Column](https://github.com/resend/react-email/tree/main/packages/column)
-- [Container](https://github.com/resend/react-email/tree/main/packages/container)
-- [Divider](https://github.com/resend/react-email/tree/main/packages/hr)
-- [Font](https://github.com/resend/react-email/tree/main/packages/font)
-- [Head](https://github.com/resend/react-email/tree/main/packages/head)
-- [Heading](https://github.com/resend/react-email/tree/main/packages/heading)
-- [Html](https://github.com/resend/react-email/tree/main/packages/html)
-- [Image](https://github.com/resend/react-email/tree/main/packages/img)
-- [Link](https://github.com/resend/react-email/tree/main/packages/link)
-- [Markdown](https://github.com/resend/react-email/tree/main/packages/markdown)
-- [Paragraph](https://github.com/resend/react-email/tree/main/packages/text)
-- [Preview](https://github.com/resend/react-email/tree/main/packages/preview)
-- [Section](https://github.com/resend/react-email/tree/main/packages/section)
+- [Html](https://github.com/resend/react-email/tree/main/packages/react-email/src/components/html)
+- [Head](https://github.com/resend/react-email/tree/main/packages/react-email/src/components/head)
+- [Button](https://github.com/resend/react-email/tree/main/packages/react-email/src/components/button)
+- [Container](https://github.com/resend/react-email/tree/main/packages/react-email/src/components/container)
+- [CodeBlock](https://github.com/resend/react-email/tree/main/packages/react-email/src/components/code-block)
+- [CodeInline](https://github.com/resend/react-email/tree/main/packages/react-email/src/components/code-inline)
+- [Column](https://github.com/resend/react-email/tree/main/packages/react-email/src/components/column)
+- [Row](https://github.com/resend/react-email/tree/main/packages/react-email/src/components/row)
+- [Font](https://github.com/resend/react-email/tree/main/packages/react-email/src/components/font)
+- [Heading](https://github.com/resend/react-email/tree/main/packages/react-email/src/components/heading)
+- [Divider](https://github.com/resend/react-email/tree/main/packages/react-email/src/components/hr)
+- [Image](https://github.com/resend/react-email/tree/main/packages/react-email/src/components/img)
+- [Link](https://github.com/resend/react-email/tree/main/packages/react-email/src/components/link)
+- [Markdown](https://github.com/resend/react-email/tree/main/packages/react-email/src/components/markdown)
+- [Preview](https://github.com/resend/react-email/tree/main/packages/react-email/src/components/preview)
+- [Section](https://github.com/resend/react-email/tree/main/packages/react-email/src/components/section)
+- [Tailwind](https://github.com/resend/react-email/tree/main/packages/react-email/src/components/tailwind)
+- [Paragraph](https://github.com/resend/react-email/tree/main/packages/react-email/src/components/text)
+- [Body](https://github.com/resend/react-email/tree/main/packages/react-email/src/components/body)
+
+## Editor
+
+React Email also provides an Editor built on top of [TipTap](https://tiptap.dev/) and [ProseMirror](https://prosemirror.net/), It serializes to React Email components, and exports email-ready HTML and plain text.
+
+See the [Editor documentation](https://react.email/docs/editor) for more details.
## Integrations
@@ -87,6 +77,8 @@ Emails built with React Email can be converted into HTML and sent using any emai
- [Resend](https://github.com/resend/react-email/tree/main/examples/resend)
- [Nodemailer](https://github.com/resend/react-email/tree/main/examples/nodemailer)
- [SendGrid](https://github.com/resend/react-email/tree/main/examples/sendgrid)
+- [MailerSend](https://github.com/resend/react-email/tree/main/examples/mailersend)
+- [Mailgun](https://github.com/resend/react-email/tree/main/examples/mailgun)
- [Postmark](https://github.com/resend/react-email/tree/main/examples/postmark)
- [AWS SES](https://github.com/resend/react-email/tree/main/examples/aws-ses)
- [Plunk](https://github.com/resend/react-email/tree/main/examples/plunk)
@@ -100,31 +92,18 @@ All components were tested using the most popular email clients.
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| Gmail ✔ | Apple Mail ✔ | Outlook ✔ | Yahoo! Mail ✔ | HEY ✔ | Superhuman ✔ |
-## Development
+## Development workflow
-#### Install dependencies
-
-```sh
-pnpm install
-```
-
-#### Build and run packages
-
-```sh
-pnpm dev
-```
-
-This will initialize all packages in parallel and watch for changes, including the website which will be available at [localhost:3000](http://localhost:3000).
+1. [Setting up your development environment](https://react.email/docs/contributing/development-workflow/1-setup)
+2. [Running tests](https://react.email/docs/contributing/development-workflow/2-running-tests)
+3. [Linting](https://react.email/docs/contributing/development-workflow/3-linting)
+4. [Building](https://react.email/docs/contributing/development-workflow/4-building)
+5. [Writing documentation](https://react.email/docs/contributing/development-workflow/5-writing-docs)
## Contributing
- [Contribution Guide](https://react.email/docs/contributing)
-## Authors
-
-- Bu Kinoshita ([@bukinoshita](https://twitter.com/bukinoshita))
-- Zeno Rocha ([@zenorocha](https://twitter.com/zenorocha))
-
-## License
+---
-MIT License
+Brought to you by [Resend](https://resend.com), MIT License.
diff --git a/apps/demo/emails/01-Barebone/activation.tsx b/apps/demo/emails/01-Barebone/activation.tsx
new file mode 100644
index 0000000000..99c66c31e1
--- /dev/null
+++ b/apps/demo/emails/01-Barebone/activation.tsx
@@ -0,0 +1,185 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Button,
+ Column,
+ Container,
+ Head,
+ Heading,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { barebonesBoxedTailwindConfig } from './theme';
+import { BarebonesFonts } from './theme-fonts';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+interface ConfirmEmailProps {
+ companyName: string;
+ url: string;
+}
+
+export const ConfirmEmail = ({ companyName, url }: ConfirmEmailProps) => (
+
+
+
+
+
+
+
+ Confirm your email address
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {companyName}
+
+
+
+
+
+
+
+
+
+ We're almost there!
+
+
+
+
+ Thank you for signing up for {companyName}.
+
+ To verify your account, we just need to confirm your email
+ address.
+
+
+
+
+
+ If you didn't request this,
+
+ please ignore this email.
+
+
+
+ {/* Footer */}
+
+
+
+
+ Barebones is the catchy slogan that perfectly encapsulates
+ the vision of our company.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 123 Market Street, Floor 1
+
+ Tech City, CA, 94102
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+
+
+
+);
+
+ConfirmEmail.PreviewProps = {
+ companyName: 'Barebones',
+ url: 'https://example.com/',
+} satisfies ConfirmEmailProps;
+
+export default ConfirmEmail;
diff --git a/apps/demo/emails/01-Barebone/feature-announcement.tsx b/apps/demo/emails/01-Barebone/feature-announcement.tsx
new file mode 100644
index 0000000000..b8c586952e
--- /dev/null
+++ b/apps/demo/emails/01-Barebone/feature-announcement.tsx
@@ -0,0 +1,306 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Button,
+ Column,
+ Container,
+ Head,
+ Heading,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { barebonesBoxedTailwindConfig } from './theme';
+import { BarebonesFonts } from './theme-fonts';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+interface FeatureAnnouncementEmailProps {
+ companyName: string;
+ url: string;
+}
+
+export const FeatureAnnouncementEmail = ({
+ companyName,
+ url,
+}: FeatureAnnouncementEmailProps) => (
+
+
+
+
+
+
+
+ Release notes — {companyName}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {companyName}
+
+
+
+
+
+
+
+
+ What's new from {companyName}
+
+
+ Release Notes
+
+
+ Learn what's shipping this month, plus other{' '}
+ {companyName} updates below.
+
+
+
+
+
+
+
+
+
+ New ways to work
+
+
+
+
+
+
+
+ Automations that save real time
+
+
+ Bring your workflows into one place, cut manual handoffs,
+ and give everyone the same source of truth.
+
+
+ Read more
+
+
+
+
+
+
+
+
+
+ A clearer view of what needs attention
+
+
+ Bring your workflows into one place, cut manual handoffs,
+ and give everyone the same source of truth.
+
+
+ Read more
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Start using {companyName}
+
+ The fastest, easiest way to use {companyName}.
+
+
+
+
+ {/* Footer */}
+
+
+
+
+ {companyName} is the catchy slogan that perfectly
+ encapsulates the vision of our company.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 123 Market Street, Floor 1
+
+ Tech City, CA, 94102
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+
+
+
+);
+
+function FeatureBlock({
+ imageUrl,
+ ctaUrl,
+ title,
+ bodyP1,
+}: {
+ imageUrl: string;
+ ctaUrl: string;
+ title: string;
+ bodyP1: string;
+}) {
+ return (
+
+
+
+ {title}
+ {bodyP1}
+
+ Try it out
+
+
+
+ );
+}
+
+FeatureAnnouncementEmail.PreviewProps = {
+ companyName: 'Barebones',
+ url: 'https://example.com/',
+} satisfies FeatureAnnouncementEmailProps;
+
+export default FeatureAnnouncementEmail;
diff --git a/apps/demo/emails/01-Barebone/password-reset.tsx b/apps/demo/emails/01-Barebone/password-reset.tsx
new file mode 100644
index 0000000000..27606f66a3
--- /dev/null
+++ b/apps/demo/emails/01-Barebone/password-reset.tsx
@@ -0,0 +1,186 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Button,
+ Column,
+ Container,
+ Head,
+ Heading,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { barebonesBoxedTailwindConfig } from './theme';
+import { BarebonesFonts } from './theme-fonts';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+interface PasswordResetEmailProps {
+ companyName: string;
+ url: string;
+}
+
+export const PasswordResetEmail = ({
+ companyName,
+ url,
+}: PasswordResetEmailProps) => (
+
+
+
+
+
+
+
+ Reset your password
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {companyName}
+
+
+
+
+
+
+
+
+
+ Reset your password
+
+
+
+
+ Someone has requested a link to change your password, and you
+ can do this through the link below.
+
+
+
+
+
+ If you didn't request this, please ignore this email.
+ Your password won't change until you access the link
+ above and create a new one.
+
+
+
+ {/* Footer */}
+
+
+
+
+ Barebones is the catchy slogan that perfectly encapsulates
+ the vision of our company.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 123 Market Street, Floor 1
+
+ Tech City, CA, 94102
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+
+
+
+);
+
+PasswordResetEmail.PreviewProps = {
+ companyName: 'Barebones',
+ url: 'https://example.com/',
+} satisfies PasswordResetEmailProps;
+
+export default PasswordResetEmail;
diff --git a/apps/demo/emails/01-Barebone/product-update.tsx b/apps/demo/emails/01-Barebone/product-update.tsx
new file mode 100644
index 0000000000..a3fb943316
--- /dev/null
+++ b/apps/demo/emails/01-Barebone/product-update.tsx
@@ -0,0 +1,468 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Button,
+ Column,
+ Container,
+ Head,
+ Heading,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { barebonesBoxedTailwindConfig } from './theme';
+import { BarebonesFonts } from './theme-fonts';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+interface ProductUpdateEmailProps {
+ companyName: string;
+ url: string;
+}
+
+export const ProductUpdateEmail = ({
+ companyName,
+ url,
+}: ProductUpdateEmailProps) => (
+
+
+
+
+
+
+
+ Product update: what's new at {companyName}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {companyName}
+
+
+
+
+
+
+
+
+ Product update
+
+
+ Here's what's new with {companyName}
+
+
+ We shipped a new release with improvements to help you move
+ faster and stay in sync. Open the dashboard to explore the
+ full changelog.
+
+
+
+ View in dashboard
+
+
+
+
+
+
+
+
+
+ Starting is easy
+
+
+
+
+
+
+
+
+ Some new things
+
+
+
+
+
+ Quality-of-life fixes
+
+
+ Expect faster load times, clearer status, and fewer clicks
+ for everyday tasks. We also tightened the spots where
+ teams tend to get stuck.
+
+
+
+
+
+ Under the hood
+
+
+ Expect faster load times, clearer status, and fewer clicks
+ for everyday tasks. We also tightened the spots where
+ teams tend to get stuck.
+
+
+
+
+
+
+
+
+
+ Some new things
+
+
+
+
+
+
+
+ Workflow improvements
+
+
+ Built for teams who need reliability at scale: clearer
+ behavior, better defaults, and less back-and-forth to
+ get work done.
+
+
+ Learn about Pro
+
+
+
+
+
+
+ Reporting & visibility
+
+
+ Built for teams who need reliability at scale: clearer
+ behavior, better defaults, and less back-and-forth to
+ get work done.
+
+
+
+ Learn about Pro
+
+
+
+
+
+
+
+
+
+
+
+
+ API & integrations
+
+
+ Built for teams who need reliability at scale: clearer
+ behavior, better defaults, and less back-and-forth to
+ get work done.
+
+
+ Learn about Pro
+
+
+
+
+
+
+
+
+
+ Some new things
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Get the app.
+
+ The fastest, easiest way to use {companyName}.
+
+
+
+
+
+ App Store
+
+
+
+
+ Google Play
+
+
+
+
+
+
+ {/* Footer */}
+
+
+
+
+ {companyName} is the catchy slogan that perfectly
+ encapsulates the vision of our company.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 123 Market Street, Floor 1
+
+ Tech City, CA, 94102
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+
+
+
+);
+
+function NumberedStep({
+ n,
+ title,
+ body,
+ last,
+}: {
+ n: string;
+ title: string;
+ body: string;
+ last?: boolean;
+}) {
+ return (
+
+
+
+
+
+
+ {title}
+
+
+ {body}
+
+
+
+ );
+}
+
+function BulletCell({ isLast }: { isLast?: boolean }) {
+ return (
+
+
+
+
+
+
+
+ These updates roll out gradually. Check your workspace to see
+ what's available to you today.
+
+
+ );
+}
+
+ProductUpdateEmail.PreviewProps = {
+ companyName: 'Barebones',
+ url: 'https://example.com/',
+} satisfies ProductUpdateEmailProps;
+
+export default ProductUpdateEmail;
diff --git a/apps/demo/emails/01-Barebone/subscription-confirmation.tsx b/apps/demo/emails/01-Barebone/subscription-confirmation.tsx
new file mode 100644
index 0000000000..134813d39c
--- /dev/null
+++ b/apps/demo/emails/01-Barebone/subscription-confirmation.tsx
@@ -0,0 +1,218 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Button,
+ Column,
+ Container,
+ Head,
+ Heading,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { barebonesBoxedTailwindConfig } from './theme';
+import { BarebonesFonts } from './theme-fonts';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+interface SubscriptionConfirmationProps {
+ companyName: string;
+ url: string;
+ userName: string;
+ planName: string;
+ planPrice: string;
+ cycleLabel: string;
+ nextBillingDate: string;
+}
+
+export const SubscriptionConfirmation = ({
+ companyName,
+ url,
+ userName,
+ planName,
+ planPrice,
+ cycleLabel,
+ nextBillingDate,
+}: SubscriptionConfirmationProps) => {
+ return (
+
+
+
+
+
+
+
+
+ You're subscribed to {companyName} {planName}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {companyName}
+
+
+
+
+
+
+
+
+
+ You're subscribed
+
+
+
+
+ Hi {userName},
+
+
+ Thank you for subscribing to {companyName} {planName}. Your
+ subscription is active—you now have access to everything
+ included in your plan.
+
+
+
+ You're billed {planPrice} per {cycleLabel}. Your next
+ charge is on {nextBillingDate}. You can update payment
+ details or cancel anytime from your account.
+
+
+
+
+ Manage subscription
+
+
+
+
+ Questions about billing or your plan?
+
+ Reply to this email—we're happy to help.
+
+
+
+ {/* Footer */}
+
+
+
+
+ Barebones is the catchy slogan that perfectly
+ encapsulates the vision of our company.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 123 Market Street, Floor 1
+
+ Tech City, CA, 94102
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+SubscriptionConfirmation.PreviewProps = {
+ companyName: 'Barebones',
+ url: 'https://example.com/',
+ userName: 'Alex',
+ planName: 'Pro',
+ planPrice: '$29',
+ cycleLabel: 'month',
+ nextBillingDate: 'April 22, 2026',
+} satisfies SubscriptionConfirmationProps;
+
+export default SubscriptionConfirmation;
diff --git a/apps/demo/emails/01-Barebone/subscription-update.tsx b/apps/demo/emails/01-Barebone/subscription-update.tsx
new file mode 100644
index 0000000000..850f9b56f2
--- /dev/null
+++ b/apps/demo/emails/01-Barebone/subscription-update.tsx
@@ -0,0 +1,218 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Button,
+ Column,
+ Container,
+ Head,
+ Heading,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { barebonesBoxedTailwindConfig } from './theme';
+import { BarebonesFonts } from './theme-fonts';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+interface SubscriptionUpdateProps {
+ companyName: string;
+ url: string;
+ userName: string;
+ planName: string;
+ planPrice: string;
+ cycleLabel: string;
+ nextBillingDate: string;
+}
+
+export const SubscriptionUpdate = ({
+ companyName,
+ url,
+ userName,
+ planName,
+ planPrice,
+ cycleLabel,
+ nextBillingDate,
+}: SubscriptionUpdateProps) => {
+ return (
+
+
+
+
+
+
+
+
+ Your {companyName} {planName} subscription was updated
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {companyName}
+
+
+
+
+
+
+
+
+
+ Your plan was updated
+
+
+
+
+ Hi {userName},
+
+
+ Your {companyName} subscription has been updated.
+ Here's a summary of your current plan and billing.
+
+
+
+ You're on {companyName} {planName} at {planPrice} per{' '}
+ {cycleLabel}. Your next charge is on {nextBillingDate}. You
+ can review invoices, update payment details, or change your
+ plan from your account settings.
+
+
+
+
+ Manage subscription
+
+
+
+
+ Something look off?
+
+ Reply to this email and we'll help sort it out.
+
+
+
+ {/* Footer */}
+
+
+
+
+ Barebones is the catchy slogan that perfectly
+ encapsulates the vision of our company.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 123 Market Street, Floor 1
+
+ Tech City, CA, 94102
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+SubscriptionUpdate.PreviewProps = {
+ companyName: 'Barebones',
+ url: 'https://example.com/',
+ userName: 'Alex',
+ planName: 'Pro',
+ planPrice: '$29',
+ cycleLabel: 'month',
+ nextBillingDate: 'April 22, 2026',
+} satisfies SubscriptionUpdateProps;
+
+export default SubscriptionUpdate;
diff --git a/apps/demo/emails/01-Barebone/text-only.tsx b/apps/demo/emails/01-Barebone/text-only.tsx
new file mode 100644
index 0000000000..5b397c15a7
--- /dev/null
+++ b/apps/demo/emails/01-Barebone/text-only.tsx
@@ -0,0 +1,188 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Column,
+ Container,
+ Head,
+ Heading,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { barebonesBoxedTailwindConfig } from './theme';
+import { BarebonesFonts } from './theme-fonts';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+const textOnlyTitle = 'A quick note from Barebones';
+
+const textOnlyParagraphs = [
+ 'This is the text-only layout: no hero image and no primary call-to-action. Use it for concise product notes, receipts, account updates, and other plain-language emails where the message should stay front and center.',
+ 'Keep paragraphs short and focused so the content is easy to scan on mobile. If you need richer visuals or a conversion button, switch to another collage template.',
+ 'Replace this placeholder copy by editing this template file directly.',
+];
+
+interface TextOnlyEmailProps {
+ companyName: string;
+ url: string;
+}
+
+export const TextOnlyEmail = ({ companyName, url }: TextOnlyEmailProps) => (
+
+
+
+
+
+
+
+ A note from {companyName}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {companyName}
+
+
+
+
+
+
+
+
+ {textOnlyParagraphs.map((block, i) => (
+
+ {block}
+
+ ))}
+
+
+
+ Open your account
+
+
+
+
+ Thanks,
+
+ The Barebones Team
+
+
+
+ {/* Footer */}
+
+
+
+
+ Barebones is the catchy slogan that perfectly encapsulates
+ the vision of our company.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 123 Market Street, Floor 1
+
+ Tech City, CA, 94102
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+
+
+
+);
+
+TextOnlyEmail.PreviewProps = {
+ companyName: 'Barebones',
+ url: 'https://example.com/',
+} satisfies TextOnlyEmailProps;
+
+export default TextOnlyEmail;
diff --git a/apps/demo/emails/01-Barebone/theme-fonts.tsx b/apps/demo/emails/01-Barebone/theme-fonts.tsx
new file mode 100644
index 0000000000..9f01fc11ab
--- /dev/null
+++ b/apps/demo/emails/01-Barebone/theme-fonts.tsx
@@ -0,0 +1,48 @@
+import { Font } from 'react-email';
+
+/**
+ * Inter variable family (weights 100–900) via Google CSS `@import`.
+ * Many webmail clients strip `@import`; the `` entries below register 400 / 500 / 600
+ * static files as a fallback when the import does not run.
+ */
+export function BarebonesFonts() {
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
diff --git a/apps/demo/emails/01-Barebone/theme.ts b/apps/demo/emails/01-Barebone/theme.ts
new file mode 100644
index 0000000000..8ac739e2bd
--- /dev/null
+++ b/apps/demo/emails/01-Barebone/theme.ts
@@ -0,0 +1,81 @@
+import type { TailwindConfig } from 'react-email';
+import plugin from 'tailwindcss/plugin';
+
+const colors = {
+ bg: '#FFFFFF',
+ 'bg-2': '#F3F4F6',
+ fg: '#14171E',
+ 'fg-2': '#43454B',
+ 'fg-3': '#7B7D81',
+ 'fg-inverted': '#FFFFFF',
+ stroke: '#F0F0F0',
+ 'stroke-strong': '#E4E4E7',
+ brand: '#614500',
+} as const;
+
+const fontScale = {
+ 11: {
+ fontSize: '11px',
+ fontWeight: '420',
+ letterSpacing: '-0.033px',
+ lineHeight: '1.5',
+ },
+ 13: {
+ fontSize: '13px',
+ fontWeight: '420',
+ letterSpacing: '-0.039px',
+ lineHeight: '1.5',
+ },
+ 14: { fontSize: '14px', fontWeight: '450', lineHeight: '1.5' },
+ 16: {
+ fontSize: '16px',
+ fontWeight: '420',
+ letterSpacing: '-0.048px',
+ lineHeight: '1.5',
+ },
+ 24: {
+ fontSize: '24px',
+ fontWeight: '600',
+ letterSpacing: '-0.084px',
+ lineHeight: '1',
+ },
+ 28: {
+ fontSize: '28px',
+ fontWeight: '600',
+ letterSpacing: '-0.084px',
+ lineHeight: '1.3',
+ },
+ 32: {
+ fontSize: '32px',
+ fontWeight: '600',
+ letterSpacing: '-0.64px',
+ lineHeight: '1.25',
+ },
+ 40: {
+ fontSize: '40px',
+ fontWeight: '600',
+ letterSpacing: '-0.8px',
+ lineHeight: '1.1',
+ },
+} as const;
+
+export const barebonesBoxedTailwindConfig: TailwindConfig = {
+ plugins: [
+ plugin(({ addUtilities, addVariant }) => {
+ addVariant('mobile', '@media (max-width: 600px)');
+ const utilities: Record> = {};
+ for (const [step, token] of Object.entries(fontScale)) {
+ utilities[`.font-${step}`] = token;
+ }
+ addUtilities(utilities);
+ }),
+ ],
+ theme: {
+ extend: {
+ colors,
+ fontFamily: {
+ sans: ['Inter', 'system-ui', 'Arial', 'sans-serif'],
+ },
+ },
+ },
+};
diff --git a/apps/demo/emails/01-Barebone/welcome.tsx b/apps/demo/emails/01-Barebone/welcome.tsx
new file mode 100644
index 0000000000..30c4003a04
--- /dev/null
+++ b/apps/demo/emails/01-Barebone/welcome.tsx
@@ -0,0 +1,301 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Button,
+ Column,
+ Container,
+ Head,
+ Heading,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { barebonesBoxedTailwindConfig } from './theme';
+import { BarebonesFonts } from './theme-fonts';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+interface WelcomeEmailProps {
+ companyName: string;
+ url: string;
+}
+
+export const WelcomeEmail = ({ companyName, url }: WelcomeEmailProps) => (
+
+
+
+
+
+
+
+ Welcome aboard—{companyName}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {companyName}
+
+
+
+
+
+
+
+
+
+
+
+ Thanks for joining us
+
+
+ Welcome to {companyName}
+
+
+ You're all set. Open your dashboard to explore the
+ basics, connect a few tools, and invite your team when
+ you're ready.
+
+
+
+
+
+
+
+
+
+
+ Getting started
+
+
+
+
+
+
+
+
+ Some new things
+
+
+
+
+
+ Team workspaces
+
+
+ Roles, guests, and access levels so the right people see
+ the right work—without extra admin overhead.
+
+
+
+
+
+ Connect your stack
+
+
+ Plug in the apps your team already uses and keep updates
+ flowing without jumping between tabs.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Start using {companyName}
+
+ The fastest, easiest way to use {companyName}.
+
+
+
+
+ {/* Footer */}
+
+
+
+
+ {companyName} is the catchy slogan that perfectly
+ encapsulates the vision of our company.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 123 Market Street, Floor 1
+
+ Tech City, CA, 94102
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+
+
+
+);
+
+function WelcomeBulletCell({ isLast }: { isLast?: boolean }) {
+ return (
+
+
+
+
+
+ Bring your team, tools, and workflows together in one place—with
+ permissions that match how you work.
+
+
+ );
+}
+
+WelcomeEmail.PreviewProps = {
+ companyName: 'Barebones',
+ url: 'https://example.com/',
+} satisfies WelcomeEmailProps;
+
+export default WelcomeEmail;
diff --git a/apps/demo/emails/02-Matte/activation.tsx b/apps/demo/emails/02-Matte/activation.tsx
new file mode 100644
index 0000000000..1c22120d10
--- /dev/null
+++ b/apps/demo/emails/02-Matte/activation.tsx
@@ -0,0 +1,175 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Button,
+ Column,
+ Container,
+ Head,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { CollageFonts } from './collage-fonts';
+import { collageTailwindConfig } from './theme';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+interface ActivationEmailProps {
+ companyName: string;
+ url: string;
+}
+
+export const ActivationEmail = ({ companyName, url }: ActivationEmailProps) => (
+
+
+
+
+
+
+
+ Confirm your email address
+
+
+
+
+
+
+
+
+
+
+ Almost there
+
+
+ Thank you for signing up for {companyName}
+
+
+ To verify your account, we just need to confirm your email
+
+
+
+
+ Confirm Email
+
+
+
+
+
+ If you didn't create an account, you can safely ignore
+ this email.
+
+
+
+ {/* Footer */}
+
+
+ Collage is the workspace where your team keeps projects,
+ context, and updates together—from first idea to launch.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 123 Market Street, Floor 1
+
+ Tech City, CA, 94102
+
+
+
+
+
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+
+
+
+);
+
+ActivationEmail.PreviewProps = {
+ companyName: 'Collage',
+ url: 'https://example.com/',
+} satisfies ActivationEmailProps;
+
+export default ActivationEmail;
diff --git a/apps/demo/emails/02-Matte/collage-fonts.tsx b/apps/demo/emails/02-Matte/collage-fonts.tsx
new file mode 100644
index 0000000000..11c9586a4b
--- /dev/null
+++ b/apps/demo/emails/02-Matte/collage-fonts.tsx
@@ -0,0 +1,38 @@
+import { Font } from 'react-email';
+
+export function CollageFonts() {
+ return (
+ <>
+
+
+
+ >
+ );
+}
diff --git a/apps/demo/emails/02-Matte/feature-announcement.tsx b/apps/demo/emails/02-Matte/feature-announcement.tsx
new file mode 100644
index 0000000000..99acc00781
--- /dev/null
+++ b/apps/demo/emails/02-Matte/feature-announcement.tsx
@@ -0,0 +1,344 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Button,
+ Column,
+ Container,
+ Head,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { CollageFonts } from './collage-fonts';
+import { collageTailwindConfig } from './theme';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+type FeatureRow = {
+ title: string;
+ description: string;
+ ctaLabel: string;
+ imageSrc: string;
+};
+
+type HighlightItem = {
+ title: string;
+ description: string;
+};
+
+interface FeatureAnnouncementEmailProps {
+ companyName: string;
+ url: string;
+}
+
+export const FeatureAnnouncementEmail = ({
+ companyName,
+ url,
+}: FeatureAnnouncementEmailProps) => {
+ const brand = companyName;
+
+ const featureLead = {
+ title: 'One workspace for the whole picture',
+ description: `Bring briefs, files, feedback, and status into ${brand} so your team spends less time chasing context across tools.`,
+ ctaLabel: "See what's new \u2192",
+ };
+
+ const featureRows: FeatureRow[] = [
+ {
+ title: 'Faster by default',
+ description:
+ 'The new flow cuts steps in half so your team can publish changes without bouncing between screens.',
+ ctaLabel: 'See how it works \u2192',
+ imageSrc: `${baseUrl}/static/collage/collage-image-6.png`,
+ },
+ {
+ title: 'Clearer for everyone',
+ description:
+ 'Permissions and activity are easier to read, so admins spend less time answering who changed this.',
+ ctaLabel: 'View the details \u2192',
+ imageSrc: `${baseUrl}/static/collage/collage-image-7.png`,
+ },
+ ];
+
+ const highlights: HighlightItem[] = [
+ {
+ title: 'One place to review',
+ description:
+ 'A single summary screen shows status, owners, and next steps for each release.',
+ },
+ {
+ title: 'Smarter notifications',
+ description:
+ 'You only get pinged when something needs your attention—not on every edit.',
+ },
+ {
+ title: 'Works with your stack',
+ description:
+ 'Connects to the tools you already use so context stays in sync across teams.',
+ },
+ ];
+
+ return (
+
+
+
+
+
+
+
+ Something new is live in {brand}
+
+
+
+
+
+
+
+
+
+
+ Meet your next favorite feature
+
+
+ We're rolling out an update that makes {brand}{' '}
+ faster, clearer, and easier to use every day.
+
+
+ It's available now—open {brand} to try it, or read on
+ for what changed and why it matters.
+
+
+
+
+
+
+
+
+
+ {featureLead.title}
+
+
+ {featureLead.description}
+
+
+
+ {featureLead.ctaLabel}
+
+
+
+
+ {featureRows.map((row, idx) => (
+ 0 ? 'mt-12' : ''}>
+
+
+
+
+
+
+ {row.title}
+
+
+ {row.description}
+
+
+
+ {row.ctaLabel}
+
+
+
+
+
+ ))}
+
+
+
+
+
+ Built from real feedback
+
+
+ Teams told us where {brand} felt slow or opaque. This
+ release tightens those loops so you spend less time
+ coordinating and more time shipping.
+
+
+
+
+ Highlights from this release:
+
+
+ {highlights.map((item, idx) => (
+
+
+
+
+ {item.title}
+
+
+ {item.description}
+
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ Collage is the workspace where your team keeps projects,
+ context, and updates together—from first idea to launch.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 123 Market Street, Floor 1
+
+ Tech City, CA, 94102
+
+
+
+
+
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+FeatureAnnouncementEmail.PreviewProps = {
+ companyName: 'Collage',
+ url: 'https://example.com/',
+} satisfies FeatureAnnouncementEmailProps;
+
+export default FeatureAnnouncementEmail;
diff --git a/apps/demo/emails/02-Matte/password-reset.tsx b/apps/demo/emails/02-Matte/password-reset.tsx
new file mode 100644
index 0000000000..b52573d961
--- /dev/null
+++ b/apps/demo/emails/02-Matte/password-reset.tsx
@@ -0,0 +1,188 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Button,
+ Column,
+ Container,
+ Head,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { CollageFonts } from './collage-fonts';
+import { collageTailwindConfig } from './theme';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+interface PasswordResetEmailProps {
+ companyName: string;
+ url: string;
+}
+
+export const PasswordResetEmail = ({
+ companyName,
+ url,
+}: PasswordResetEmailProps) => (
+
+
+
+
+
+
+
+ Reset your password
+
+
+
+
+
+
+
+
+
+
+ Reset your password
+
+
+ Someone requested a password reset for your {companyName}{' '}
+ account. Use the button below to choose a new password.
+
+
+
+
+ Change password
+
+
+
+
+
+ If you didn't request this, please ignore this email.
+ Your password won't change until you access the link
+ above and create a new one.
+
+
+
+
+
+ Collage is the workspace where your team keeps projects,
+ context, and updates together—from first idea to launch.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 123 Market Street, Floor 1
+
+ Tech City, CA, 94102
+
+
+
+
+
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+
+
+
+);
+
+PasswordResetEmail.PreviewProps = {
+ companyName: 'Collage',
+ url: 'https://example.com/',
+} satisfies PasswordResetEmailProps;
+
+export default PasswordResetEmail;
diff --git a/apps/demo/emails/02-Matte/product-update.tsx b/apps/demo/emails/02-Matte/product-update.tsx
new file mode 100644
index 0000000000..27e457b75c
--- /dev/null
+++ b/apps/demo/emails/02-Matte/product-update.tsx
@@ -0,0 +1,335 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Button,
+ Column,
+ Container,
+ Head,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { CollageFonts } from './collage-fonts';
+import { collageTailwindConfig } from './theme';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+type FeatureCard = {
+ title: string;
+ description: string;
+ cta: string;
+ imageSrc: string;
+};
+
+type UpdateItem = {
+ title: string;
+ description: string;
+};
+
+interface ProductUpdateEmailProps {
+ companyName: string;
+ url: string;
+}
+
+export const ProductUpdateEmail = ({
+ companyName,
+ url,
+}: ProductUpdateEmailProps) => {
+ const brand = companyName;
+
+ const cards: FeatureCard[] = [
+ {
+ title: 'Smarter Search',
+ description:
+ 'Search results are now grouped by intent and ranked by relevance, so teams find the right content faster.',
+ cta: 'Read the update \u2192',
+ imageSrc: `${baseUrl}/static/collage/collage-image-6.png`,
+ },
+ {
+ title: 'Shared Workspaces',
+ description:
+ 'You can now organize projects into shared workspaces with clearer ownership and cleaner handoffs.',
+ cta: 'See what changed \u2192',
+ imageSrc: `${baseUrl}/static/collage/collage-image-7.png`,
+ },
+ ];
+
+ const items: UpdateItem[] = [
+ {
+ title: 'New Help Center',
+ description:
+ 'A faster docs experience with guided setup, troubleshooting paths, and deeper API references.',
+ },
+ {
+ title: 'Permissions',
+ description:
+ 'Granular role controls let admins manage access by workspace, project, and action.',
+ },
+ {
+ title: 'New Discovery Tools',
+ description:
+ 'Saved views and smart filters make it easier to surface new ideas and high-signal activity.',
+ },
+ ];
+
+ return (
+
+
+
+
+
+
+
+ Your monthly {brand} product updates
+
+
+
+
+
+
+
+
+
+
+ Monthly roundup
+
+
+ Here's what shipped this month across {brand}.
+
+
+ We focused on speed, collaboration, and control to help
+ your team move from idea to launch faster.
+
+
+
+
+
+
+
+
+ {cards.slice(0, 2).map((card, idx) => (
+
+
+
+
+
+ {card.title}
+
+
+ {card.description}
+
+
+ {card.cta}
+
+
+
+
+ ))}
+
+
+
+
+
+
+ Something to be proud of
+
+
+ From quality-of-life fixes to major workflow improvements,
+ this release is built around feedback from teams using{' '}
+ {brand} every day.
+
+
+
+
+ Here's the newest from the month:
+
+
+ {items.map((item, idx) => (
+
+
+
+
+ {item.title}
+
+
+ {item.description}
+
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+ Jump back into {brand}
+
+
+ You're all set—open {brand} to explore what's new
+ and keep projects moving.
+
+
+
+
+
+
+
+
+
+ Collage is the workspace where your team keeps projects,
+ context, and updates together—from first idea to launch.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 123 Market Street, Floor 1
+
+ Tech City, CA, 94102
+
+
+
+
+
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+ProductUpdateEmail.PreviewProps = {
+ companyName: 'Collage',
+ url: 'https://example.com/',
+} satisfies ProductUpdateEmailProps;
+
+export default ProductUpdateEmail;
diff --git a/apps/demo/emails/02-Matte/subscription-confirmation.tsx b/apps/demo/emails/02-Matte/subscription-confirmation.tsx
new file mode 100644
index 0000000000..983c11d828
--- /dev/null
+++ b/apps/demo/emails/02-Matte/subscription-confirmation.tsx
@@ -0,0 +1,315 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Button,
+ Column,
+ Container,
+ Head,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { CollageFonts } from './collage-fonts';
+import { collageTailwindConfig } from './theme';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+interface SubscriptionConfirmationProps {
+ companyName: string;
+ url: string;
+ userName: string;
+ planName: string;
+ planPrice: string;
+ cycleLabel: string;
+ nextBillingDate: string;
+ subtotal: string;
+ tax: string;
+ total: string;
+}
+
+export const SubscriptionConfirmation = ({
+ companyName,
+ url,
+ userName,
+ planName,
+ planPrice,
+ cycleLabel,
+ nextBillingDate,
+ subtotal,
+ tax,
+ total,
+}: SubscriptionConfirmationProps) => {
+ return (
+
+
+
+
+
+
+
+
+ You're subscribed to {companyName} {planName}
+
+
+
+
+
+
+
+
+
+
+
+ Subscription confirmed
+
+
+ Hi {userName}, thanks for subscribing to the {planName}{' '}
+ plan with {companyName}. Your subscription is active and
+ you have access to everything included in your plan.
+
+
+ You're billed {planPrice} per {cycleLabel}. Your next
+ charge is on {nextBillingDate}. You can update payment
+ details or cancel anytime from your account.
+
+
+ Questions about billing or your plan? Reply to this email
+ and we're happy to help.
+
+
+
+
+
+
+
+
+ Subtotal
+
+
+
+
+ {subtotal}
+
+
+
+
+
+
+
+
+ Tax
+
+
+
+
+ {tax}
+
+
+
+
+
+
+
+
+ Total
+
+
+
+
+ {total}
+
+
+
+
+
+
+
+ Manage subscription
+
+
+
+
+ Get started
+
+
+
+
+ Set up your workspace
+
+
+ Complete the basics to get the most out of your account.
+
+
+ Complete Setup
+
+
+
+
+
+ Invite your team
+
+
+ Collaboration works best when everyone's in.
+
+
+ Invite Teammates
+
+
+
+
+
+ Need help?
+
+
+ Find guides, tips, and best practices anytime, visit our{' '}
+
+ Help Center
+
+ .
+
+
+
+
+
+
+
+ Collage is the workspace where your team keeps projects,
+ context, and updates together—from first idea to launch.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 123 Market Street, Floor 1
+
+ Tech City, CA, 94102
+
+
+
+
+
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+SubscriptionConfirmation.PreviewProps = {
+ companyName: 'Collage',
+ url: 'https://example.com/',
+ userName: 'Alex',
+ planName: 'Pro',
+ planPrice: '$29',
+ cycleLabel: 'month',
+ nextBillingDate: 'April 22, 2026',
+ subtotal: '$29.00',
+ tax: '$0.00',
+ total: '$29.00',
+} satisfies SubscriptionConfirmationProps;
+
+export default SubscriptionConfirmation;
diff --git a/apps/demo/emails/02-Matte/subscription-update.tsx b/apps/demo/emails/02-Matte/subscription-update.tsx
new file mode 100644
index 0000000000..fbb8c2a9d8
--- /dev/null
+++ b/apps/demo/emails/02-Matte/subscription-update.tsx
@@ -0,0 +1,213 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Button,
+ Column,
+ Container,
+ Head,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { CollageFonts } from './collage-fonts';
+import { collageTailwindConfig } from './theme';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+interface SubscriptionUpdateProps {
+ companyName: string;
+ url: string;
+ userName: string;
+ planName: string;
+ planPrice: string;
+ cycleLabel: string;
+ nextBillingDate: string;
+}
+
+export const SubscriptionUpdate = ({
+ companyName,
+ url,
+ userName,
+ planName,
+ planPrice,
+ cycleLabel,
+ nextBillingDate,
+}: SubscriptionUpdateProps) => {
+ return (
+
+
+
+
+
+
+
+
+ Your {companyName} plan renewed ({planName})
+
+
+
+
+
+
+
+
+
+
+
+ Plan renewed
+
+
+ Hi {userName}. Your {companyName} subscription renewed for
+ another {cycleLabel}. Here's a quick summary of your
+ plan and billing.
+
+
+ You're on the {planName} plan at {planPrice} per{' '}
+ {cycleLabel}. Your next charge is on {nextBillingDate}.
+ Review invoices, update payment details, or change plans
+ anytime in your account.
+
+
+ Something look off? Reply to this email and we'll
+ help sort it out.
+
+
+
+
+ Manage subscription
+
+
+
+
+
+ Collage is the workspace where your team keeps projects,
+ context, and updates together—from first idea to launch.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 123 Market Street, Floor 1
+
+ Tech City, CA, 94102
+
+
+
+
+
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+SubscriptionUpdate.PreviewProps = {
+ companyName: 'Collage',
+ url: 'https://example.com/',
+ userName: 'Alex',
+ planName: 'Pro',
+ planPrice: '$29',
+ cycleLabel: 'month',
+ nextBillingDate: 'April 22, 2026',
+} satisfies SubscriptionUpdateProps;
+
+export default SubscriptionUpdate;
diff --git a/apps/demo/emails/02-Matte/text-only.tsx b/apps/demo/emails/02-Matte/text-only.tsx
new file mode 100644
index 0000000000..63ab00aab9
--- /dev/null
+++ b/apps/demo/emails/02-Matte/text-only.tsx
@@ -0,0 +1,183 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Column,
+ Container,
+ Head,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { CollageFonts } from './collage-fonts';
+import { collageTailwindConfig } from './theme';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+interface TextOnlyEmailProps {
+ companyName: string;
+ url: string;
+}
+
+export const TextOnlyEmail = ({ companyName, url }: TextOnlyEmailProps) => {
+ return (
+
+
+
+
+
+
+ A short note from the {companyName} team
+
+
+
+
+
+ A note from us
+
+
+
+ We don't send long emails often—when we do, we want
+ them to feel direct. No hero image, no noise: just a quick
+ word from the team behind {companyName}.
+
+
+ We're building {companyName} so your team can keep
+ projects, context, and updates in one place. If
+ you've been in the product lately, thank you—what we
+ hear from you shapes what we ship next.
+
+
+ Here's to fewer tabs and clearer handoffs. When
+ you're ready to dive back in, we'll be there.
+
+
+ — The {companyName} team
+
+
+
+ Open your workspace
+
+
+
+
+
+
+
+ {companyName} is the workspace where your team keeps
+ projects, context, and updates together—from first idea to
+ launch.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 123 Market Street, Floor 1
+
+ Tech City, CA, 94102
+
+
+
+
+
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+TextOnlyEmail.PreviewProps = {
+ companyName: 'Collage',
+ url: 'https://example.com/',
+} satisfies TextOnlyEmailProps;
+
+export default TextOnlyEmail;
diff --git a/apps/demo/emails/02-Matte/theme.ts b/apps/demo/emails/02-Matte/theme.ts
new file mode 100644
index 0000000000..1c368822db
--- /dev/null
+++ b/apps/demo/emails/02-Matte/theme.ts
@@ -0,0 +1,67 @@
+import type { TailwindConfig } from 'react-email';
+import plugin from 'tailwindcss/plugin';
+
+const colors = {
+ canvas: '#FBFCFB',
+ bg: '#FFFFFF',
+ 'bg-2': '#FBFCFB',
+ fg: '#103B05',
+ 'fg-2': '#194A07',
+ 'fg-3': '#869C7F',
+ 'fg-inverted': '#FBFFF9',
+ stroke: '#D8E1D4',
+ brand: '#103B05',
+} as const;
+
+const fontScale = {
+ 11: {
+ fontSize: '11px',
+ lineHeight: '1.5',
+ letterSpacing: '-0.033px',
+ fontWeight: '300',
+ },
+ 13: {
+ fontSize: '13px',
+ lineHeight: '1.5',
+ letterSpacing: '-0.039px',
+ fontWeight: '300',
+ },
+ 14: { fontSize: '14px', lineHeight: '1.5' },
+ 15: {
+ fontSize: '15px',
+ lineHeight: '1.5',
+ letterSpacing: '-0.075px',
+ fontWeight: '500',
+ },
+ 20: { fontSize: '20px', lineHeight: '1.2', letterSpacing: '-0.2px' },
+ 32: { fontSize: '32px', lineHeight: '1.2', letterSpacing: '-0.6px' },
+ 48: { fontSize: '48px', lineHeight: '1', letterSpacing: '-1.44px' },
+ 58: { fontSize: '58px', lineHeight: '1', letterSpacing: '-1.74px' },
+ 88: { fontSize: '88px', lineHeight: '1', letterSpacing: '-2.64px' },
+} as const;
+
+export const collageTailwindConfig: TailwindConfig = {
+ plugins: [
+ plugin(({ addUtilities, addVariant }) => {
+ addVariant('mobile', '@media (max-width: 600px)');
+ const utilities: Record> = {};
+ for (const [step, token] of Object.entries(fontScale)) {
+ utilities[`.font-${step}`] = token;
+ }
+ addUtilities(utilities);
+ }),
+ ],
+ theme: {
+ extend: {
+ colors,
+ boxShadow: {
+ 'collage-card':
+ '0px 76px 21px 0px rgba(193,195,193,0), 0px 49px 19px 0px rgba(193,195,193,0.01), 0px 27px 16px 0px rgba(193,195,193,0.05), 0px 12px 12px 0px rgba(193,195,193,0.09), 0px 3px 7px 0px rgba(193,195,193,0.1)',
+ },
+ fontFamily: {
+ sans: ['Arial', 'Helvetica', 'sans-serif'],
+ inter: ['Inter', 'Arial', 'sans-serif'],
+ },
+ },
+ },
+};
diff --git a/apps/demo/emails/02-Matte/welcome.tsx b/apps/demo/emails/02-Matte/welcome.tsx
new file mode 100644
index 0000000000..2837bffc18
--- /dev/null
+++ b/apps/demo/emails/02-Matte/welcome.tsx
@@ -0,0 +1,264 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Button,
+ Column,
+ Container,
+ Head,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { CollageFonts } from './collage-fonts';
+import { collageTailwindConfig } from './theme';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+type WelcomeTip = {
+ title: string;
+ description: string;
+};
+
+interface WelcomeEmailProps {
+ companyName: string;
+ url: string;
+}
+
+export const WelcomeEmail = ({ companyName, url }: WelcomeEmailProps) => {
+ const brand = companyName;
+ const welcomeTitle = `Welcome to ${brand}`;
+
+ const tips: WelcomeTip[] = [
+ {
+ title: 'Complete your profile',
+ description:
+ 'Add a photo and a short bio so teammates recognize you in comments and mentions.',
+ },
+ {
+ title: 'Turn on notifications',
+ description:
+ 'Choose email or in-app alerts so you never miss a review request or deadline.',
+ },
+ {
+ title: 'Browse templates',
+ description:
+ 'Starter layouts and snippets help your team ship polished work without reinventing the wheel.',
+ },
+ ];
+
+ return (
+
+
+
+
+
+
+
+ Welcome to {brand}
+
+
+
+
+
+
+
+
+
+
+ {welcomeTitle}
+
+
+ Thank you for signing up for {brand}.
+
+
+ You're all set—explore what's new and get your
+ first project going.
+
+
+
+
+
+
+
+
+
+ Your first week in {brand}
+
+
+ Small steps add up. Use this short checklist to get
+ comfortable—everything here is optional, but it helps you
+ feel at home faster.
+
+
+
+
+ Here's what to try first:
+
+
+ {tips.map((item, idx) => (
+
+
+
+
+ {item.title}
+
+
+ {item.description}
+
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ Collage is the workspace where your team keeps projects,
+ context, and updates together—from first idea to launch.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 123 Market Street, Floor 1
+
+ Tech City, CA, 94102
+
+
+
+
+
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+WelcomeEmail.PreviewProps = {
+ companyName: 'Collage',
+ url: 'https://example.com/',
+} satisfies WelcomeEmailProps;
+
+export default WelcomeEmail;
diff --git a/apps/demo/emails/03-Protocol/activation.tsx b/apps/demo/emails/03-Protocol/activation.tsx
new file mode 100644
index 0000000000..d01c0b07a2
--- /dev/null
+++ b/apps/demo/emails/03-Protocol/activation.tsx
@@ -0,0 +1,184 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Button,
+ Column,
+ Container,
+ Head,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { DitherFonts } from './dither-fonts';
+import { ditherTailwindConfig } from './theme';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+interface ActivationEmailProps {
+ companyName: string;
+ url: string;
+}
+
+export const ActivationEmail = ({ companyName, url }: ActivationEmailProps) => (
+
+
+
+
+
+
+
+ Confirm your email address
+
+
+
+
+
+
+
+
+
+
+
+ almost there
+
+
+ Thank you for signing up for {companyName}.
+
+
+ To verify your account, we just need to confirm your email.
+
+
+ If you didn't create an account, you can safely ignore this
+ email.
+
+
+
+ Confirm Email
+
+
+
+ {/* Footer */}
+
+
+ {companyName} helps teams cut through noise—clear priorities,
+ fewer tabs, and less busywork from idea to shipped work.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 123 Market Street, Floor 1
+
+ Tech City, CA, 94102
+
+
+
+
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+
+);
+
+ActivationEmail.PreviewProps = {
+ companyName: 'Dither',
+ url: 'https://example.com/',
+} satisfies ActivationEmailProps;
+
+export default ActivationEmail;
diff --git a/apps/demo/emails/03-Protocol/dither-fonts.tsx b/apps/demo/emails/03-Protocol/dither-fonts.tsx
new file mode 100644
index 0000000000..3009df04ef
--- /dev/null
+++ b/apps/demo/emails/03-Protocol/dither-fonts.tsx
@@ -0,0 +1,48 @@
+import { Font } from 'react-email';
+
+export function DitherFonts() {
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
diff --git a/apps/demo/emails/03-Protocol/feature-announcement.tsx b/apps/demo/emails/03-Protocol/feature-announcement.tsx
new file mode 100644
index 0000000000..afb3e1d90b
--- /dev/null
+++ b/apps/demo/emails/03-Protocol/feature-announcement.tsx
@@ -0,0 +1,307 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Button,
+ Column,
+ Container,
+ Head,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { DitherFonts } from './dither-fonts';
+import { ditherTailwindConfig } from './theme';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+interface FeatureAnnouncementEmailProps {
+ companyName: string;
+ url: string;
+}
+
+export const FeatureAnnouncementEmail = ({
+ companyName,
+ url,
+}: FeatureAnnouncementEmailProps) => {
+ const featureName = 'Smart Tasks';
+ const heroImageSrc = `${baseUrl}/static/dither/dither-image-2.png`;
+
+ return (
+
+
+
+
+
+
+
+ Meet {featureName}
+
+
+
+
+
+
+
+
+
+
+ Meet a new way to manage work
+
+
+ Introducing {featureName}: a calmer way to organize work,
+ surface what matters next, and keep your team aligned without
+ the noise.
+
+
+
+ Explore {featureName}
+
+
+
+
+
+
+ Meet smart tasks
+
+
+
+
+ Auto-prioritization
+
+
+ All your tasks for the week are surfaced based on urgency and
+ impact.
+
+
+ Explore {featureName}
+
+
+
+ {['Less manual work', 'Clear focus', 'Better alignment'].map(
+ (title, idx) => (
+
+
+
+
+
+
+
+ {title}
+
+
+ {idx === 0
+ ? 'Fewer status updates, more automated work.'
+ : idx === 1
+ ? 'Always know what to do next—less thrash, more momentum.'
+ : 'Your team stays in sync automatically.'}
+
+
+
+
+ ),
+ )}
+
+
+
+ Smart Tasks analyzes your projects in real time and helps
+ organize{' '}
+
+
+ your workload automatically. As things change, your task list
+ updates so you're always focused on what matters most.
+
+
+
+
+
+
+
+ "Smart Tasks helps me know what to do next without
+ thinking about it."
+
+
+ Alex, Product Designer at Studio
+
+
+
+
+
+
+ Try smart tasks
+
+
+ See how much easier work can be.
+
+
+
+ Explore {featureName}
+
+
+
+
+
+
+ Need help?
+
+
+ Find guides, tips, and best practices anytime, visit our{' '}
+
+ Help Center
+
+ .
+
+
+
+
+ {/* Footer */}
+
+
+ {companyName} helps teams cut through noise—clear priorities,
+ fewer tabs, and less busywork from idea to shipped work.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 123 Market Street, Floor 1
+
+ Tech City, CA, 94102
+
+
+
+
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+
+ );
+};
+
+FeatureAnnouncementEmail.PreviewProps = {
+ companyName: 'Dither',
+ url: 'https://example.com/',
+} satisfies FeatureAnnouncementEmailProps;
+
+export default FeatureAnnouncementEmail;
diff --git a/apps/demo/emails/03-Protocol/password-reset.tsx b/apps/demo/emails/03-Protocol/password-reset.tsx
new file mode 100644
index 0000000000..a66e168e63
--- /dev/null
+++ b/apps/demo/emails/03-Protocol/password-reset.tsx
@@ -0,0 +1,177 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Button,
+ Column,
+ Container,
+ Head,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { DitherFonts } from './dither-fonts';
+import { ditherTailwindConfig } from './theme';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+interface PasswordResetEmailProps {
+ companyName: string;
+ url: string;
+}
+
+export const PasswordResetEmail = ({
+ companyName,
+ url,
+}: PasswordResetEmailProps) => (
+
+
+
+
+
+
+
+ Reset your password
+
+
+
+
+
+
+
+
+ password reset
+
+
+ We received a request to reset your password for {companyName}.
+
+
+ If you didn't request a reset, you can safely ignore this
+ email.
+
+
+
+
+ Create New Password
+
+
+
+ {/* Footer */}
+
+
+ {companyName} helps teams cut through noise—clear priorities,
+ fewer tabs, and less busywork from idea to shipped work.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 123 Market Street, Floor 1
+
+ Tech City, CA, 94102
+
+
+
+
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+
+);
+
+PasswordResetEmail.PreviewProps = {
+ companyName: 'Dither',
+ url: 'https://example.com/',
+} satisfies PasswordResetEmailProps;
+
+export default PasswordResetEmail;
diff --git a/apps/demo/emails/03-Protocol/product-update.tsx b/apps/demo/emails/03-Protocol/product-update.tsx
new file mode 100644
index 0000000000..af76cb7955
--- /dev/null
+++ b/apps/demo/emails/03-Protocol/product-update.tsx
@@ -0,0 +1,323 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Column,
+ Container,
+ Head,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { DitherFonts } from './dither-fonts';
+import { ditherTailwindConfig } from './theme';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+type ProductUpdateFeature = {
+ title: string;
+ body: string;
+ linkText: string;
+ linkHref: string;
+ imageSrc: string;
+};
+
+const productUpdateHeadline = 'THE WORK BEHIND THE WORK';
+
+interface ProductUpdateEmailProps {
+ companyName: string;
+ url: string;
+}
+
+export const ProductUpdateEmail = ({
+ companyName,
+ url,
+}: ProductUpdateEmailProps) => {
+ const featureRows: ProductUpdateFeature[] = [
+ {
+ title: 'Auto-prioritization',
+ body: 'All your tasks for the week are surfaced based on urgency and impact.',
+ linkText: 'Explore Smart Tasks',
+ linkHref: url,
+ imageSrc: `${baseUrl}/static/dither/dither-image-7.png`,
+ },
+ {
+ title: 'Less manual work',
+ body: 'Fewer status updates, more automated work.',
+ linkText: 'Read more',
+ linkHref: url,
+ imageSrc: `${baseUrl}/static/dither/dither-image-4.png`,
+ },
+ {
+ title: 'Less manual work',
+ body: 'Fewer status updates, more automated work.',
+ linkText: 'Read more',
+ linkHref: url,
+ imageSrc: `${baseUrl}/static/dither/dither-image-5.png`,
+ },
+ ];
+
+ return (
+
+
+
+
+
+
+ What's new in {companyName}
+
+
+
+
+
+
+
+
+
+
+
+ {productUpdateHeadline}
+
+
+
+
+
+
+
+
+ Here from the {companyName} team.
+
+
+ This month was about making {companyName} feel smoother, faster,
+ and easier to use in everyday work.
+
+
+ We shipped new features, improved key workflows, and made small
+ but meaningful updates that reduce friction across the product.
+
+
+ A lot of our focus was on simplifying actions, improving
+ performance, and responding to feedback from teams using the
+ product day to day.
+
+
+ We're continuing to shape {companyName} around real
+ workflows—not just features—so everything feels more connected
+ and effortless over time.
+
+
+
+
+
+ WE LAUNCHED SMART TASKS
+
+
+
+
+ {featureRows[0].title}
+
+
+ {featureRows[0].body}
+
+
+ {featureRows[0].linkText}
+
+
+
+
+
+
+
+ Smart Tasks analyzes your projects in real time and helps
+ organize{' '}
+
+
+ your workload automatically. As things change, your task list
+ updates so you're always focused on what matters most.
+
+
+
+
+
+
+ READ MORE ON OUR BLOG
+
+
+
+
+
+
+
+ {featureRows[1].title}
+
+
+ {featureRows[1].body}
+
+
+ {featureRows[1].linkText}
+
+
+
+
+
+
+
+ {featureRows[2].title}
+
+
+ {featureRows[2].body}
+
+
+ {featureRows[2].linkText}
+
+
+
+
+
+
+
+
+ Need help?
+
+ Find guides, tips, and best practices anytime, visit our Help
+ Center.
+
+
+
+ {/* Footer */}
+
+
+ {companyName} helps teams cut through noise—clear priorities,
+ fewer tabs, and less busywork from idea to shipped work.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 123 Market Street, Floor 1
+
+ Tech City, CA, 94102
+
+
+
+
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+
+ );
+};
+
+ProductUpdateEmail.PreviewProps = {
+ companyName: 'Dither',
+ url: 'https://example.com/',
+} satisfies ProductUpdateEmailProps;
+
+export default ProductUpdateEmail;
diff --git a/apps/demo/emails/03-Protocol/subscription-confirmation.tsx b/apps/demo/emails/03-Protocol/subscription-confirmation.tsx
new file mode 100644
index 0000000000..69d3b92d47
--- /dev/null
+++ b/apps/demo/emails/03-Protocol/subscription-confirmation.tsx
@@ -0,0 +1,262 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Button,
+ Column,
+ Container,
+ Head,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { DitherFonts } from './dither-fonts';
+import { ditherTailwindConfig } from './theme';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+interface SubscriptionConfirmationProps {
+ companyName: string;
+ url: string;
+ planName: string;
+ userName: string;
+ nextBillingDate: string;
+}
+
+export const SubscriptionConfirmation = ({
+ companyName,
+ url,
+ planName,
+ userName,
+ nextBillingDate,
+}: SubscriptionConfirmationProps) => {
+ return (
+
+
+
+
+
+
+
+
+ You're on {planName} with {companyName}
+
+
+
+
+
+
+
+
+
+
+
+ welcome to {planName}
+
+
+ Thanks for starting your {planName} subscription, {userName}.
+
+
+ Your payment method has been charged. The next charge will be
+ on{' '}
+
+ {nextBillingDate}.
+
+
+
+ You can modify your payment method or cancel your subscription
+ anytime by visiting the {companyName}{' '}
+
+ billing settings
+ {' '}
+ page.
+
+
+
+ Open {companyName}
+
+
+
+
+
+ Get started
+
+
+
+
+ Set up your workspace
+
+
+ Complete the basics to get the most out of your account.
+
+
+ Complete Setup
+
+
+
+
+
+ Invite your team
+
+
+ Collaboration works best when everyone's in.
+
+
+ Invite Teammates
+
+
+
+
+
+ Need help?
+
+
+ Find guides, tips, and best practices anytime, visit our{' '}
+
+ Help Center
+
+ .
+
+
+
+
+ {/* Footer */}
+
+
+ {companyName} helps teams cut through noise—clear priorities,
+ fewer tabs, and less busywork from idea to shipped work.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 123 Market Street, Floor 1
+
+ Tech City, CA, 94102
+
+
+
+
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+
+ );
+};
+
+SubscriptionConfirmation.PreviewProps = {
+ companyName: 'Dither',
+ url: 'https://example.com/',
+ planName: 'Pro',
+ userName: 'Alex',
+ nextBillingDate: 'April 22, 2026',
+} satisfies SubscriptionConfirmationProps;
+
+export default SubscriptionConfirmation;
diff --git a/apps/demo/emails/03-Protocol/subscription-update.tsx b/apps/demo/emails/03-Protocol/subscription-update.tsx
new file mode 100644
index 0000000000..976902c27c
--- /dev/null
+++ b/apps/demo/emails/03-Protocol/subscription-update.tsx
@@ -0,0 +1,193 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Button,
+ Column,
+ Container,
+ Head,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { DitherFonts } from './dither-fonts';
+import { ditherTailwindConfig } from './theme';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+interface SubscriptionUpdateProps {
+ companyName: string;
+ url: string;
+ userName: string;
+ planName: string;
+ nextBillingDate: string;
+}
+
+export const SubscriptionUpdate = ({
+ companyName,
+ url,
+ userName,
+ planName,
+ nextBillingDate,
+}: SubscriptionUpdateProps) => {
+ return (
+
+
+
+
+
+
+
+
+ Your {planName} plan with {companyName} renewed
+
+
+
+
+
+
+
+
+ Plan renewed
+
+
+ Hi {userName}, your subscription has been renewed.
+
+
+ Your payment method has been charged. The next charge will be
+ on {nextBillingDate}. You can modify your payment method or
+ cancel your subscription anytime by visiting the {companyName}{' '}
+ billing settings page.
+
+
+
+ Manage subscription
+
+
+
+ {/* Footer */}
+
+
+ {companyName} helps teams cut through noise—clear priorities,
+ fewer tabs, and less busywork from idea to shipped work.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 123 Market Street, Floor 1
+
+ Tech City, CA, 94102
+
+
+
+
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+
+ );
+};
+
+SubscriptionUpdate.PreviewProps = {
+ companyName: 'Dither',
+ url: 'https://example.com/',
+ userName: 'Name Surname',
+ planName: 'Pro',
+ nextBillingDate: 'Jun 15, 2026',
+} satisfies SubscriptionUpdateProps;
+
+export default SubscriptionUpdate;
diff --git a/apps/demo/emails/03-Protocol/text-only.tsx b/apps/demo/emails/03-Protocol/text-only.tsx
new file mode 100644
index 0000000000..f46ed739a7
--- /dev/null
+++ b/apps/demo/emails/03-Protocol/text-only.tsx
@@ -0,0 +1,188 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Column,
+ Container,
+ Head,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { DitherFonts } from './dither-fonts';
+import { ditherTailwindConfig } from './theme';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+interface TextOnlyEmailProps {
+ companyName: string;
+ url: string;
+}
+
+export const TextOnlyEmail = ({ companyName, url }: TextOnlyEmailProps) => {
+ return (
+
+
+
+
+
+
+ A short note from the {companyName} team
+
+
+
+
+
+
+
+ A note from us
+
+
+
+ We don't send long emails often. When we do, we keep them
+ plain—no hero, no clutter—just a direct word from the team
+ behind {companyName}.
+
+
+ We're building {companyName} so your team can see what
+ matters, drop what doesn't, and ship without living in
+ fifteen tabs. If you've been in the product lately, thank
+ you—your feedback sharpens what we ship next.
+
+
+ Here's to clearer priorities and less noise. When
+ you're ready to jump back in, we'll be there.
+
+
+ — The {companyName} team
+
+
+
+ Open {companyName}
+
+
+
+
+
+
+
+ {companyName} helps teams cut through noise—clear priorities,
+ fewer tabs, and less busywork from idea to shipped work.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 123 Market Street, Floor 1
+
+ Tech City, CA, 94102
+
+
+
+
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+
+ );
+};
+
+TextOnlyEmail.PreviewProps = {
+ companyName: 'Dither',
+ url: 'https://example.com/',
+} satisfies TextOnlyEmailProps;
+
+export default TextOnlyEmail;
diff --git a/apps/demo/emails/03-Protocol/theme.ts b/apps/demo/emails/03-Protocol/theme.ts
new file mode 100644
index 0000000000..1608b2298f
--- /dev/null
+++ b/apps/demo/emails/03-Protocol/theme.ts
@@ -0,0 +1,97 @@
+import type { TailwindConfig } from 'react-email';
+import plugin from 'tailwindcss/plugin';
+
+const colors = {
+ bg: '#131313',
+ 'bg-2': '#212121',
+ fg: '#FFFFFF',
+ 'fg-2': '#C4C4C4',
+ 'fg-3': '#818181',
+ 'fg-inverted': '#FFFFFF',
+ stroke: '#2B2B2B',
+ brand: '#614500',
+ muted: '#4A4A4A',
+ 'subtle-border': '#D0D0D0',
+ surface: '#FFFFFF',
+ ink: '#131313',
+} as const;
+
+const fontScale = {
+ 11: {
+ fontSize: '11px',
+ lineHeight: '1.5',
+ letterSpacing: '0.3px',
+ fontWeight: '300',
+ },
+ 13: {
+ fontSize: '13px',
+ lineHeight: '1.5',
+ letterSpacing: '0.2px',
+ fontWeight: '300',
+ },
+ 14: {
+ fontSize: '14px',
+ lineHeight: '1.5',
+ letterSpacing: '0.3px',
+ fontWeight: '350',
+ },
+ 15: {
+ fontSize: '15px',
+ lineHeight: '1.5',
+ letterSpacing: '-0.075px',
+ fontWeight: '450',
+ },
+ 20: { fontSize: '20px', lineHeight: '1.1', fontWeight: '500' },
+ 24: {
+ fontSize: '24px',
+ lineHeight: '1.5',
+ letterSpacing: '-0.072px',
+ fontWeight: '500',
+ },
+ 32: {
+ fontSize: '32px',
+ lineHeight: '0.9',
+ letterSpacing: '0.4px',
+ fontWeight: '500',
+ },
+ 40: {
+ fontSize: '40px',
+ lineHeight: '1',
+ letterSpacing: '-1.2px',
+ fontWeight: '500',
+ },
+ 56: {
+ fontSize: '56px',
+ lineHeight: '1',
+ letterSpacing: '-1.68px',
+ fontWeight: '500',
+ },
+} as const;
+
+export const ditherTailwindConfig: TailwindConfig = {
+ plugins: [
+ plugin(({ addUtilities, addVariant }) => {
+ addVariant('mobile', '@media (max-width: 600px)');
+ const utilities: Record> = {};
+ for (const [step, token] of Object.entries(fontScale)) {
+ utilities[`.font-${step}`] = token;
+ }
+ addUtilities(utilities);
+ }),
+ ],
+ theme: {
+ extend: {
+ colors,
+ fontFamily: {
+ sans: ['Inter', 'Arial', 'sans-serif'],
+ mono: ["'IBM Plex Mono'", "'Courier New'", 'monospace'],
+ condensed: [
+ "'IBM Plex Sans Condensed'",
+ "'Arial Narrow'",
+ 'Arial',
+ 'sans-serif',
+ ],
+ },
+ },
+ },
+};
diff --git a/apps/demo/emails/03-Protocol/welcome.tsx b/apps/demo/emails/03-Protocol/welcome.tsx
new file mode 100644
index 0000000000..b619a16886
--- /dev/null
+++ b/apps/demo/emails/03-Protocol/welcome.tsx
@@ -0,0 +1,222 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Column,
+ Container,
+ Head,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { DitherFonts } from './dither-fonts';
+import { ditherTailwindConfig } from './theme';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+interface WelcomeEmailProps {
+ companyName: string;
+ url: string;
+}
+
+export const WelcomeEmail = ({ companyName, url }: WelcomeEmailProps) => (
+
+
+
+
+
+
+ Welcome to {companyName}
+
+
+
+
+
+
+
+ Welcome to {companyName}
+
+
+ You can start exploring right away, set up your workspace, and
+ invite your team if you're working with others.
+
+
+
+
+
+
+
+
+
+
+ If you need help getting started, we've got you covered. Find
+ guides, tips, and best practices anytime, visit our{' '}
+
+ Help Center
+
+ .
+
+
+
+
+
+ Get started
+
+
+
+
+ Set up your workspace
+
+
+ Complete the basics to get the most out of your account.
+
+
+ Complete Setup
+
+
+
+
+
+ Invite your team
+
+
+ Collaboration works best when everyone's in.
+
+
+ Invite Teammates
+
+
+
+
+ Need help?
+
+ Find guides, tips, and best practices anytime, visit our{' '}
+
+ Help Center
+
+ .
+
+
+
+
+ {/* Footer */}
+
+
+ {companyName} helps teams cut through noise—clear priorities,
+ fewer tabs, and less busywork from idea to shipped work.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 123 Market Street, Floor 1
+
+ Tech City, CA, 94102
+
+
+
+
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+
+);
+
+WelcomeEmail.PreviewProps = {
+ companyName: 'Dither',
+ url: 'https://example.com/',
+} satisfies WelcomeEmailProps;
+
+export default WelcomeEmail;
diff --git a/apps/demo/emails/04-Arcane/abandoned-cart.tsx b/apps/demo/emails/04-Arcane/abandoned-cart.tsx
new file mode 100644
index 0000000000..aa78f36d80
--- /dev/null
+++ b/apps/demo/emails/04-Arcane/abandoned-cart.tsx
@@ -0,0 +1,236 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Column,
+ Container,
+ Head,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { SkinFonts } from './skin-fonts';
+import { skinTailwindConfig } from './theme';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+type CartLine = {
+ name: string;
+ description: string;
+ price: string;
+ imageSrc: string;
+};
+interface AbandonedCartEmailProps {
+ companyName: string;
+ url: string;
+}
+const abandonedCartLines: CartLine[] = [
+ {
+ name: 'Daily C Serum',
+ description:
+ 'Brightening daytime serum—still in your cart. Finish checkout before we release the inventory hold.',
+ price: '$99',
+ imageSrc: `${baseUrl}/static/skin/skin-image-2.png`,
+ },
+ {
+ name: 'Overnight Repair Cream',
+ description:
+ 'Pairs with your morning serum for AM/PM balance. Limited batches move fast on restocks.',
+ price: '$99',
+ imageSrc: `${baseUrl}/static/skin/skin-image-3.png`,
+ },
+];
+export const AbandonedCartEmail = ({
+ companyName,
+ url,
+}: AbandonedCartEmailProps) => {
+ return (
+
+
+
+
+
+
+
+ Your {companyName} cart is waiting
+
+
+
+
+
+
+
+
+
+
+ Pick up where you left off
+
+
+ Your cart still holds these formulas—checkout soon before
+ limited stock or your hold window expires.
+
+
+ {abandonedCartLines.slice(0, 2).map((line, idx) => (
+
+
+
+
+
+
+
+
+ {line.name}
+
+
+ {line.description}
+
+
+ {line.price}
+
+
+
+
+ ))}
+
+
+ {'Return to cart \u2192'}
+
+
+
+
+
+ Don’t take it from us
+
+
+
+
+ Join us on the journey
+
+
+ There's more ahead—new drops, member perks, and restock
+ alerts. Explore what's new and stay in the loop while
+ your cart is on hold.
+
+
+
+ {'Start Exploring \u2192'}
+
+
+
+
+
+ {/* Footer */}
+
+
+ {companyName} crafts thoughtful skincare—barrier-first
+ formulas, honest labels, and routines you'll actually
+ keep.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 124 Mercantile Row, Studio 3
+
+ Los Angeles, CA, 90013
+
+
+
+
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+
+
+ );
+};
+
+AbandonedCartEmail.PreviewProps = {
+ companyName: 'Skin',
+ url: 'https://example.com/',
+} satisfies AbandonedCartEmailProps;
+
+export default AbandonedCartEmail;
diff --git a/apps/demo/emails/04-Arcane/activation.tsx b/apps/demo/emails/04-Arcane/activation.tsx
new file mode 100644
index 0000000000..213e1dc70b
--- /dev/null
+++ b/apps/demo/emails/04-Arcane/activation.tsx
@@ -0,0 +1,184 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Column,
+ Container,
+ Head,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { SkinFonts } from './skin-fonts';
+import { skinTailwindConfig } from './theme';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+interface ActivationEmailProps {
+ companyName: string;
+ url: string;
+}
+export const ActivationEmail = ({ companyName, url }: ActivationEmailProps) => (
+
+
+
+
+
+
+
+ Confirm your email address
+
+
+
+
+
+
+
+
+
+
+
+ Almost there
+
+
+ Thank you for signing up for {companyName}.
+
+
+ To verify your account, we just need to confirm your email.
+
+
+
+
+ {'Confirm email \u2192'}
+
+
+
+
+
+ If you didn't create an account with {companyName}, you can
+ ignore this email—your inbox won't be added to our list.
+
+
+
+ {/* Footer */}
+
+
+ {companyName} crafts thoughtful skincare—barrier-first formulas,
+ honest labels, and routines you'll actually keep.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 124 Mercantile Row, Studio 3
+
+ Los Angeles, CA, 90013
+
+
+
+
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+
+
+);
+
+ActivationEmail.PreviewProps = {
+ companyName: 'Skin',
+ url: 'https://example.com/',
+} satisfies ActivationEmailProps;
+
+export default ActivationEmail;
diff --git a/apps/demo/emails/04-Arcane/newsletter.tsx b/apps/demo/emails/04-Arcane/newsletter.tsx
new file mode 100644
index 0000000000..9f69661c7f
--- /dev/null
+++ b/apps/demo/emails/04-Arcane/newsletter.tsx
@@ -0,0 +1,306 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Column,
+ Container,
+ Head,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { SkinFonts } from './skin-fonts';
+import { skinTailwindConfig } from './theme';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+type NewsletterTip = {
+ step: number;
+ title: string;
+ body: string;
+ ctaLabel: string;
+};
+type NewsletterCommunity = {
+ imageSrc: string;
+ headline: string;
+ body: string;
+ ctaLabel: string;
+};
+interface NewsletterEmailProps {
+ companyName: string;
+ url: string;
+}
+const newsletterLetterParagraphs = [
+ "We're glad you're here. This is where we share routine notes, ingredient spotlights, and first word on drops—plus the occasional restock alert before shelves clear.",
+ 'No inbox clutter—just emails when we have something worth your time. Thanks for letting us sit next to your morning serum and your evening wind-down.',
+];
+const newsletterTips: NewsletterTip[] = [
+ {
+ step: 1,
+ title: 'Start with a patch test',
+ body: "Try new textures on a small area first, especially if you're rotating in actives or seasonal formulas.",
+ ctaLabel: 'Learn more \u2192',
+ },
+ {
+ step: 2,
+ title: 'Layer for your climate',
+ body: "Humidity, cold snaps, and travel all change how skin drinks product layers in heat, richer barriers when it's dry.",
+ ctaLabel: 'Learn more \u2192',
+ },
+ {
+ step: 3,
+ title: 'Track what works',
+ body: 'Note how your skin feels after AM and PM routines so you can double down on what actually moves the needle.',
+ ctaLabel: 'Learn more \u2192',
+ },
+];
+
+const newsletterCommunity: NewsletterCommunity = {
+ imageSrc: `${baseUrl}/static/skin/skin-image-5.png`,
+ headline:
+ 'Most members say one or two routine swaps moved the needle more than a dozen impulse buys.',
+ body: 'Peek behind the formulas—short guides, founder notes, and community answers so you shop for your skin, not the algorithm.',
+ ctaLabel: 'See what\u2019s new \u2192',
+};
+export const NewsletterEmail = ({ companyName, url }: NewsletterEmailProps) => {
+ const quoteText =
+ 'Shopping with ' +
+ companyName +
+ ' finally made my counter feel calm—every product earns its spot, and my skin stays predictable through travel and stress.';
+ return (
+
+
+
+
+
+
+
+ The latest from {companyName}
+
+
+
+
+
+
+
+ Newsletter
+
+
+ Hi there,
+
+ {newsletterLetterParagraphs.map((p, i) => (
+
+ {p}
+
+ ))}
+
+ Thanks for reading, The {companyName} team
+
+
+
+
+
+
+
+
+ Tips and resources
+
+ {newsletterTips.map((tip, idx) => (
+
+
+
+
+
+
+
+
+
+
+ {tip.title}
+
+
+
+
+
+
+
+ {tip.body}
+
+
+
+
+
+ ))}
+
+
+
+
+ “{quoteText}”
+
+
+
+ Alex Morgan
+
+
+ Member since 2024
+
+
+
+
+
+
+
+
+
+
+ {newsletterCommunity.headline}
+
+
+ {newsletterCommunity.body}
+
+
+
+ {newsletterCommunity.ctaLabel}
+
+
+
+
+
+ {/* Footer */}
+
+
+ {companyName} crafts thoughtful skincare—barrier-first
+ formulas, honest labels, and routines you'll actually
+ keep.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 124 Mercantile Row, Studio 3
+
+ Los Angeles, CA, 90013
+
+
+
+
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+
+
+ );
+};
+
+NewsletterEmail.PreviewProps = {
+ companyName: 'Skin',
+ url: 'https://example.com/',
+} satisfies NewsletterEmailProps;
+
+export default NewsletterEmail;
diff --git a/apps/demo/emails/04-Arcane/order-confirmation.tsx b/apps/demo/emails/04-Arcane/order-confirmation.tsx
new file mode 100644
index 0000000000..b00a6c3a2f
--- /dev/null
+++ b/apps/demo/emails/04-Arcane/order-confirmation.tsx
@@ -0,0 +1,272 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Column,
+ Container,
+ Head,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { SkinFonts } from './skin-fonts';
+import { skinTailwindConfig } from './theme';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+export type OrderLine = { name: string; quantity: string; imageSrc: string };
+interface OrderConfirmationEmailProps {
+ companyName: string;
+ url: string;
+}
+const orderConfirmationLines: OrderLine[] = [
+ {
+ name: 'Daily C Serum',
+ quantity: 'x1',
+ imageSrc: `${baseUrl}/static/skin/skin-image-2.png`,
+ },
+ {
+ name: 'Overnight Repair Cream',
+ quantity: 'x1',
+ imageSrc: `${baseUrl}/static/skin/skin-image-3.png`,
+ },
+];
+export const OrderConfirmationEmail = ({
+ companyName,
+ url,
+}: OrderConfirmationEmailProps) => {
+ return (
+
+
+
+
+
+
+
+ Your {companyName} order #1234567890 is confirmed
+
+
+
+
+
+
+
+
+ Your order has been placed
+
+
+ Your order #1234567890 has been confirmed!
+
+
+ We'll send tracking as soon as your parcel leaves our
+ studio, usually within one business day.
+
+
+
+ {'Track your order \u2192'}
+
+
+
+
+ {orderConfirmationLines.map((line, idx) => (
+
+ {idx > 0 ? (
+
+ ) : null}
+
+
+
+
+
+
+
+ {line.name}
+
+
+ {line.quantity}
+
+
+
+
+ ))}
+
+
+
+
+
+
+ Subtotal
+
+
+
+
+ $198.00
+
+
+
+
+
+
+ Tax
+
+
+
+
+ $16.00
+
+
+
+
+
+
+ Shipping
+
+
+
+
+ $0.00
+
+
+
+
+
+
+ Total
+
+
+
+
+ $214.00
+
+
+
+
+
+
+
+ {/* Footer */}
+
+
+ {companyName} crafts thoughtful skincare—barrier-first
+ formulas, honest labels, and routines you'll actually
+ keep.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 124 Mercantile Row, Studio 3
+
+ Los Angeles, CA, 90013
+
+
+
+
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+
+
+ );
+};
+
+OrderConfirmationEmail.PreviewProps = {
+ companyName: 'Skin',
+ url: 'https://example.com/',
+} satisfies OrderConfirmationEmailProps;
+
+export default OrderConfirmationEmail;
diff --git a/apps/demo/emails/04-Arcane/order-shipping.tsx b/apps/demo/emails/04-Arcane/order-shipping.tsx
new file mode 100644
index 0000000000..e797f9b161
--- /dev/null
+++ b/apps/demo/emails/04-Arcane/order-shipping.tsx
@@ -0,0 +1,263 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Column,
+ Container,
+ Head,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import type { OrderLine } from './order-confirmation';
+import { SkinFonts } from './skin-fonts';
+import { skinTailwindConfig } from './theme';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+type ShippingFaqItem = {
+ title: string;
+ linkLabel: string;
+ linkHref: string;
+};
+
+interface OrderShippingEmailProps {
+ companyName: string;
+ url: string;
+}
+
+const orderShippingLines: OrderLine[] = [
+ {
+ name: 'Daily C Serum',
+ quantity: 'x1',
+ imageSrc: `${baseUrl}/static/skin/skin-image-2.png`,
+ },
+ {
+ name: 'Overnight Repair Cream',
+ quantity: 'x1',
+ imageSrc: `${baseUrl}/static/skin/skin-image-3.png`,
+ },
+];
+
+const getOrderShippingFaqItems = (url: string): ShippingFaqItem[] => [
+ {
+ title: 'Need to change ship date?',
+ linkLabel: 'See how \u2192',
+ linkHref: url,
+ },
+ {
+ title: 'Sending to a new address?',
+ linkLabel: 'See how \u2192',
+ linkHref: url,
+ },
+];
+
+export const OrderShippingEmail = ({
+ companyName,
+ url,
+}: OrderShippingEmailProps) => {
+ return (
+
+
+
+
+
+
+
+ Your {companyName} order #1234567890 has shipped
+
+
+
+
+
+
+
+
+ Your order has shipped
+
+
+ Order #1234567890 is on the way.
+
+
+ Track your shipment below, we'll email if the carrier
+ reroutes or weather slows delivery.
+
+
+
+ {'Track your order \u2192'}
+
+
+
+
+ {orderShippingLines.map((line, idx) => (
+
+ {idx > 0 ? (
+
+ ) : null}
+
+
+
+
+
+
+
+ {line.name}
+
+
+ {line.quantity}
+
+
+
+
+ ))}
+
+
+
+
+
+ {'View order \u2192'}
+
+
+
+
+
+
+ Common questions
+
+
+ {getOrderShippingFaqItems(url).map((item, idx) => (
+
+
+
+
+ {item.title}
+
+
+
+
+
+ ))}
+
+
+
+ {/* Footer */}
+
+
+ {companyName} crafts thoughtful skincare—barrier-first
+ formulas, honest labels, and routines you'll actually
+ keep.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 124 Mercantile Row, Studio 3
+
+ Los Angeles, CA, 90013
+
+
+
+
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+
+
+ );
+};
+
+OrderShippingEmail.PreviewProps = {
+ companyName: 'Skin',
+ url: 'https://example.com/',
+} satisfies OrderShippingEmailProps;
+
+export default OrderShippingEmail;
diff --git a/apps/demo/emails/04-Arcane/password-reset.tsx b/apps/demo/emails/04-Arcane/password-reset.tsx
new file mode 100644
index 0000000000..995a7aa9e6
--- /dev/null
+++ b/apps/demo/emails/04-Arcane/password-reset.tsx
@@ -0,0 +1,176 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Column,
+ Container,
+ Head,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { SkinFonts } from './skin-fonts';
+import { skinTailwindConfig } from './theme';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+interface PasswordResetEmailProps {
+ companyName: string;
+ url: string;
+}
+export const PasswordResetEmail = ({
+ companyName,
+ url,
+}: PasswordResetEmailProps) => (
+
+
+
+
+
+
+
+ Reset your password
+
+
+
+
+
+
+
+
+
+ Reset your Password
+
+
+ We received a request to reset your password for {companyName}
+ .
+
+
+ Use the link below to choose a new password. If you
+ didn't request this, you can ignore this email.
+
+
+
+
+ {'Reset password \u2192'}
+
+
+
+
+ {/* Footer */}
+
+
+ {companyName} crafts thoughtful skincare—barrier-first formulas,
+ honest labels, and routines you'll actually keep.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 124 Mercantile Row, Studio 3
+
+ Los Angeles, CA, 90013
+
+
+
+
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+
+
+);
+
+PasswordResetEmail.PreviewProps = {
+ companyName: 'Skin',
+ url: 'https://example.com/',
+} satisfies PasswordResetEmailProps;
+
+export default PasswordResetEmail;
diff --git a/apps/demo/emails/04-Arcane/promo.tsx b/apps/demo/emails/04-Arcane/promo.tsx
new file mode 100644
index 0000000000..f76d8e89cc
--- /dev/null
+++ b/apps/demo/emails/04-Arcane/promo.tsx
@@ -0,0 +1,284 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Column,
+ Container,
+ Head,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { SkinFonts } from './skin-fonts';
+import { skinTailwindConfig } from './theme';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+type PromoFeature = {
+ title: string;
+ body: string;
+ ctaLabel: string;
+ imageSrc: string;
+ imagePosition: 'left' | 'right';
+};
+interface PromoEmailProps {
+ companyName: string;
+ url: string;
+}
+
+const promoFeatures: PromoFeature[] = [
+ {
+ title: 'Bright morning serum',
+ body: 'Stabilized vitamin C and humectants to wake up dull skin—smooth after cleansing, lock in with SPF for daytime.',
+ ctaLabel: 'Shop the serum \u2192',
+ imageSrc: `${baseUrl}/static/skin/skin-image-6.png`,
+ imagePosition: 'left',
+ },
+ {
+ title: 'Barrier night cream',
+ body: 'Ceramides and peptides support overnight recovery when skin feels tight, flushed, or stressed by city air.',
+ ctaLabel: 'Shop the cream \u2192',
+ imageSrc: `${baseUrl}/static/skin/skin-image-7.png`,
+ imagePosition: 'right',
+ },
+];
+
+export const PromoEmail = ({ companyName, url }: PromoEmailProps) => {
+ return (
+
+
+
+
+
+
+
+ Your {companyName} promo code
+
+
+
+
+
+
+
+
+
+
+ Use code
+
+
+ SKIN15
+
+
+
+ {'Use promo \u2192'}
+
+
+
+
+
+
+
+
+ Some new things
+
+
+ {promoFeatures.map((feature, idx) => {
+ const isImgLeft = feature.imagePosition === 'left';
+ const imgBlock = (
+
+
+
+ );
+ const textBlock = (
+
+
+
+ {feature.title}
+
+
+ {feature.body}
+
+
+
+ {feature.ctaLabel}
+
+
+
+
+ );
+ const gap = (
+
+ );
+ return (
+ 0 ? 'mobile:mt-10 mt-[64px]' : ''}
+ >
+
+ {feature.imagePosition === 'left' ? (
+ <>
+ {imgBlock} {gap} {textBlock}
+ >
+ ) : (
+ <>
+ {textBlock} {gap} {imgBlock}
+ >
+ )}
+
+
+ );
+ })}
+
+
+
+
+
+
+ Join us on the journey
+
+
+ There's more ahead—new drops, member perks, and restock
+ alerts. Explore what's new and stay in the loop while
+ your cart is on hold.
+
+
+
+ {'Start Exploring \u2192'}
+
+
+
+
+
+ {/* Footer */}
+
+
+ {companyName} crafts thoughtful skincare—barrier-first
+ formulas, honest labels, and routines you'll actually
+ keep.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 124 Mercantile Row, Studio 3
+
+ Los Angeles, CA, 90013
+
+
+
+
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+
+
+ );
+};
+
+PromoEmail.PreviewProps = {
+ companyName: 'Skin',
+ url: 'https://example.com/',
+} satisfies PromoEmailProps;
+
+export default PromoEmail;
diff --git a/apps/demo/emails/04-Arcane/skin-fonts.tsx b/apps/demo/emails/04-Arcane/skin-fonts.tsx
new file mode 100644
index 0000000000..ce6e36fddb
--- /dev/null
+++ b/apps/demo/emails/04-Arcane/skin-fonts.tsx
@@ -0,0 +1,47 @@
+import { Font } from 'react-email';
+export function SkinFonts() {
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
diff --git a/apps/demo/emails/04-Arcane/theme.ts b/apps/demo/emails/04-Arcane/theme.ts
new file mode 100644
index 0000000000..35983076e5
--- /dev/null
+++ b/apps/demo/emails/04-Arcane/theme.ts
@@ -0,0 +1,73 @@
+import type { TailwindConfig } from 'react-email';
+import plugin from 'tailwindcss/plugin';
+
+const colors = {
+ bg: '#300610',
+ 'bg-2': '#431D26',
+ 'bg-3': '#F9F9ED',
+ fg: '#FCF3ED',
+ 'fg-2': '#EFE1D8',
+ 'fg-3': '#B09996',
+ 'fg-inverted': '#300610',
+ stroke: '#3D151D',
+ brand: '#614500',
+} as const;
+
+const fontScale = {
+ 11: { fontSize: '11px', lineHeight: '1.5', letterSpacing: '0.3px' },
+ 13: { fontSize: '13px', lineHeight: '1.5', letterSpacing: '0.2px' },
+ 15: { fontSize: '15px', lineHeight: '1.5', letterSpacing: '0.25px' },
+ 16: {
+ fontSize: '16px',
+ lineHeight: '1.5',
+ letterSpacing: '0.168px',
+ fontWeight: '500',
+ },
+ 18: { fontSize: '18px', lineHeight: '1.5', letterSpacing: '0.056px' },
+ 20: { fontSize: '20px', lineHeight: '1.5', letterSpacing: '0.1px' },
+ 22: {
+ fontSize: '22px',
+ lineHeight: '1.5',
+ letterSpacing: '-0.176px',
+ fontWeight: '500',
+ },
+ 24: {
+ fontSize: '24px',
+ lineHeight: '1.4',
+ letterSpacing: '0.008px',
+ fontWeight: '500',
+ },
+ 32: {
+ fontSize: '32px',
+ lineHeight: '1.4',
+ letterSpacing: '0.008px',
+ fontWeight: '500',
+ },
+ 40: { fontSize: '40px', lineHeight: '1', letterSpacing: '-0.2px' },
+ 48: { fontSize: '48px', lineHeight: '1.2', letterSpacing: '-0.28px' },
+ 56: { fontSize: '56px', lineHeight: '1', letterSpacing: '-0.36px' },
+ 64: { fontSize: '64px', lineHeight: '1', letterSpacing: '-0.56px' },
+ 72: { fontSize: '72px', lineHeight: '1', letterSpacing: '-0.52px' },
+} as const;
+
+export const skinTailwindConfig: TailwindConfig = {
+ plugins: [
+ plugin(({ addUtilities, addVariant }) => {
+ addVariant('mobile', '@media (max-width: 600px)');
+ const utilities: Record> = {};
+ for (const [step, token] of Object.entries(fontScale)) {
+ utilities[`.font-${step}`] = token;
+ }
+ addUtilities(utilities);
+ }),
+ ],
+ theme: {
+ extend: {
+ colors,
+ fontFamily: {
+ sans: ['Inter', 'Arial', 'sans-serif'],
+ serif: ["'Instrument Serif'", 'Georgia', "'Times New Roman'", 'serif'],
+ },
+ },
+ },
+};
diff --git a/apps/demo/emails/04-Arcane/welcome.tsx b/apps/demo/emails/04-Arcane/welcome.tsx
new file mode 100644
index 0000000000..773cf4598c
--- /dev/null
+++ b/apps/demo/emails/04-Arcane/welcome.tsx
@@ -0,0 +1,282 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Column,
+ Container,
+ Head,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { SkinFonts } from './skin-fonts';
+import { skinTailwindConfig } from './theme';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+type WelcomeFeature = {
+ title: string;
+ body: string;
+ ctaLabel: string;
+ imageSrc: string;
+ imagePosition: 'left' | 'right';
+};
+interface WelcomeEmailProps {
+ companyName: string;
+ url: string;
+}
+const welcomeFeatures: WelcomeFeature[] = [
+ {
+ title: 'Build your ritual',
+ body: 'Layer textures and finishes that match your routine—morning calm, evening wind-down, or both.',
+ ctaLabel: 'Learn more \u2192',
+ imageSrc: `${baseUrl}/static/skin/skin-image-6.png`,
+ imagePosition: 'left',
+ },
+ {
+ title: 'Restocks & drops',
+ body: 'Members hear first when favorites return and limited runs go live—stay in the loop from day one.',
+ ctaLabel: 'Learn more \u2192',
+ imageSrc: `${baseUrl}/static/skin/skin-image-7.png`,
+ imagePosition: 'right',
+ },
+];
+export const WelcomeEmail = ({ companyName, url }: WelcomeEmailProps) => {
+ return (
+
+
+
+
+
+
+
+ Welcome to {companyName}
+
+
+
+
+
+
+
+
+ Welcome
+
+
+ Thank you for joining {companyName}. Your shelf just got a
+ little smarter.
+
+
+ Unlock member perks, restock alerts, and checkout that
+ remembers your routine.
+
+
+
+
+
+
+
+
+
+ Some new things
+
+
+ {welcomeFeatures.map((feature, idx) => {
+ const isImgLeft = feature.imagePosition === 'left';
+ const imgBlock = (
+
+
+
+ );
+ const textBlock = (
+
+
+
+ {feature.title}
+
+
+ {feature.body}
+
+
+
+ {feature.ctaLabel}
+
+
+
+
+ );
+ const gap = (
+
+ );
+ return (
+ 0 ? 'mobile:mt-10 mt-[64px]' : ''}
+ >
+
+ {feature.imagePosition === 'left' ? (
+ <>
+ {imgBlock} {gap} {textBlock}
+ >
+ ) : (
+ <>
+ {textBlock} {gap} {imgBlock}
+ >
+ )}
+
+
+ );
+ })}
+
+
+
+
+
+
+ Join us on the journey
+
+
+ There's more ahead—new drops, member perks, and restock
+ alerts. Explore what's new and stay in the loop while
+ your cart is on hold.
+
+
+
+ {'Start Exploring \u2192'}
+
+
+
+
+
+ {/* Footer */}
+
+
+ {companyName} crafts thoughtful skincare—barrier-first
+ formulas, honest labels, and routines you'll actually
+ keep.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 124 Mercantile Row, Studio 3
+
+ Los Angeles, CA, 90013
+
+
+
+
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+
+
+ );
+};
+
+WelcomeEmail.PreviewProps = {
+ companyName: 'Skin',
+ url: 'https://example.com/',
+} satisfies WelcomeEmailProps;
+
+export default WelcomeEmail;
diff --git a/apps/demo/emails/05-Studio/abandoned-cart.tsx b/apps/demo/emails/05-Studio/abandoned-cart.tsx
new file mode 100644
index 0000000000..70edcf417a
--- /dev/null
+++ b/apps/demo/emails/05-Studio/abandoned-cart.tsx
@@ -0,0 +1,230 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Button,
+ Column,
+ Container,
+ Head,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { TechFonts } from './tech-fonts';
+import { techTailwindConfig } from './theme';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+interface TechAbandonedCartEmailProps {
+ companyName: string;
+ url: string;
+}
+
+type BenefitItem = {
+ title: string;
+};
+
+const techAbandonedCartBenefits: BenefitItem[] = [
+ { title: 'Try your ring risk-free 30 days' },
+ { title: 'Free shipping and easy returns' },
+ { title: 'Two-year ring warranty included' },
+];
+
+export const TechAbandonedCartEmail = ({
+ companyName,
+ url,
+}: TechAbandonedCartEmailProps) => (
+
+
+
+
+
+
+
+ Your Halo ring cart is waiting
+
+
+
+
+
+
+
+
+
+ Pick up where you left off
+
+
+ Your cart still holds your Halo ring—we saved your size and
+ finish. Check out soon before inventory shifts or your hold
+ window expires.
+
+
+
+ Return to cart
+
+
+
+
+
+
+
+
+
+
+
+
+ Halo Ring 1
+
+
+ x1
+
+
+
+
+
+
+
+
+
+ Don't take it from us
+
+
+
+
+ ★★★★★
+
+ “It's wild how fast Halo on my finger felt like
+ calm help whenever my head was full and my hands were
+ busy.”
+
+
+
+
+
+ {techAbandonedCartBenefits.map((item, idx) => (
+
+
+ {item.title}
+
+
+ ))}
+
+
+
+
+
+
+
+ {/*Footer*/}
+
+
+
+ {companyName} is the AI ring on your finger—easy shopping, clear
+ shipping, and real support when you need it.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 123 Market Street, Floor 1
+
+ Tech City, CA, 94102
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+);
+
+TechAbandonedCartEmail.PreviewProps = {
+ companyName: 'Halo',
+ url: 'https://example.com/',
+} satisfies TechAbandonedCartEmailProps;
+
+export default TechAbandonedCartEmail;
diff --git a/apps/demo/emails/05-Studio/activation.tsx b/apps/demo/emails/05-Studio/activation.tsx
new file mode 100644
index 0000000000..fb7284a831
--- /dev/null
+++ b/apps/demo/emails/05-Studio/activation.tsx
@@ -0,0 +1,156 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Button,
+ Column,
+ Container,
+ Head,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { TechFonts } from './tech-fonts';
+import { techTailwindConfig } from './theme';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+interface TechActivationEmailProps {
+ companyName: string;
+ url: string;
+}
+
+export const TechActivationEmail = ({
+ companyName,
+ url,
+}: TechActivationEmailProps) => (
+
+
+
+
+
+
+
+ Confirm your email address
+
+
+
+
+
+
+
+
+
+ One last step
+
+
+ Thanks for creating your {companyName} account.
+
+ To shop, track ring orders, and get updates, please confirm
+ this email address.
+
+
+
+
+ Confirm Email
+
+
+
+
+
+
+
+ {companyName} is the AI ring on your finger—easy shopping, clear
+ shipping, and real support when you need it.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 123 Market Street, Floor 1
+
+ Tech City, CA, 94102
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+);
+
+TechActivationEmail.PreviewProps = {
+ companyName: 'Halo',
+ url: 'https://example.com/',
+} satisfies TechActivationEmailProps;
+
+export default TechActivationEmail;
diff --git a/apps/demo/emails/05-Studio/newsletter.tsx b/apps/demo/emails/05-Studio/newsletter.tsx
new file mode 100644
index 0000000000..9ef57f044a
--- /dev/null
+++ b/apps/demo/emails/05-Studio/newsletter.tsx
@@ -0,0 +1,306 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Button,
+ Column,
+ Container,
+ Head,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { TechFonts } from './tech-fonts';
+import { techTailwindConfig } from './theme';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+const letterMaxWidthClass = 'max-w-[430px]';
+
+type TechNewsletterTip = {
+ title: string;
+ body: string;
+ ctaLabel: string;
+ imageSrc: string;
+};
+
+type TechNewsletterSpotlight = {
+ title: string;
+ body: string;
+ imageSrc: string;
+ buttonLabel: string;
+};
+
+type TechNewsletterCommunity = {
+ imageSrc: string;
+ headline: string;
+ body: string;
+ ctaLabel: string;
+};
+
+interface TechNewsletterEmailProps {
+ companyName: string;
+ url: string;
+}
+
+const techNewsletterLetterParagraphs = [
+ "We're glad you're here. This is our space for Halo ring drops, sizing tips, and the stories behind each batch—we share what we're building, what we're learning, and when limited finishes return.",
+ "No inbox noise—just emails when restocks land, firmware improves, or there's something meaningful for members. Thanks for reading; we're happy you're along for the journey.",
+];
+
+const techNewsletterTips: TechNewsletterTip[] = [
+ {
+ title: 'Pick the right ring size online',
+ body: 'Use our printable sizer or visit a partner store so your Halo sits snug—returns are easier when the first fit is close.',
+ ctaLabel: 'Read More \u2192',
+ imageSrc: `${baseUrl}/static/tech/tech-image-4.png`,
+ },
+ {
+ title: 'Keep your AI assistant subtle',
+ body: 'Toggle haptics and voice replies in the app so meetings stay quiet while you still get the nudges that matter.',
+ ctaLabel: 'Read More \u2192',
+ imageSrc: `${baseUrl}/static/tech/tech-image-4.png`,
+ },
+];
+
+const techNewsletterSpotlight: TechNewsletterSpotlight = {
+ title: 'Your personal AI companion, wrapped around your finger.',
+ body: 'Halo learns how you shop, move, and focus—then surfaces gentle prompts, order updates, and wellness cues without another screen.',
+ imageSrc: `${baseUrl}/static/tech/tech-image-3.png`,
+ buttonLabel: 'Read More \u2192',
+};
+
+const techNewsletterCommunity: TechNewsletterCommunity = {
+ imageSrc: `${baseUrl}/static/tech/tech-image-2.png`,
+ headline: "Intelligence that doesn't demand attention.",
+ body: "Shop your finish and size—we'll confirm inventory and ship fast.",
+ ctaLabel: 'Shop Halo',
+};
+
+/** Tech newsletter — same content blocks as Skin `newsletter.tsx`, light card layout (Figma Email-Templates family). */
+export const TechNewsletterEmail = ({
+ companyName,
+ url,
+}: TechNewsletterEmailProps) => {
+ return (
+
+
+
+
+
+
+
+ The latest from {companyName}
+
+
+
+
+
+
+
+
+ Why your finger is the calmest screen
+
+
+ {techNewsletterLetterParagraphs.map((p, i) => (
+
+ {p}
+
+ ))}
+
+
+
+
+
+
+
+
+
+ {techNewsletterSpotlight.title}
+
+
+ {techNewsletterSpotlight.body}
+
+
+
+ {techNewsletterSpotlight.buttonLabel}
+
+
+
+
+
+
+
+ From the Halo journal
+
+
+
+
+
+
+ {techNewsletterTips.map((tip, idx) => (
+
+
+
+
+
+
+ {tip.title}
+
+
+ {tip.body}
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+ {techNewsletterCommunity.headline}
+
+
+ {techNewsletterCommunity.body}
+
+
+
+ {techNewsletterCommunity.ctaLabel}
+
+
+
+
+
+
+
+ {companyName} is the AI ring on your finger—easy shopping,
+ clear shipping, and real support when you need it.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 123 Market Street, Floor 1
+
+ Tech City, CA, 94102
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+
+ );
+};
+
+TechNewsletterEmail.PreviewProps = {
+ companyName: 'Halo',
+ url: 'https://example.com/',
+} satisfies TechNewsletterEmailProps;
+
+export default TechNewsletterEmail;
diff --git a/apps/demo/emails/05-Studio/order-confirmation.tsx b/apps/demo/emails/05-Studio/order-confirmation.tsx
new file mode 100644
index 0000000000..0097232fd5
--- /dev/null
+++ b/apps/demo/emails/05-Studio/order-confirmation.tsx
@@ -0,0 +1,262 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Button,
+ Column,
+ Container,
+ Head,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { TechFonts } from './tech-fonts';
+import { techTailwindConfig } from './theme';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+export type OrderLine = {
+ name: string;
+ quantity: string;
+ imageSrc: string;
+};
+
+interface TechOrderConfirmationEmailProps {
+ companyName: string;
+ url: string;
+}
+
+/** Figma Email-Templates `2681:2383` — Tech “order placed” card (Geist headline, line items, summary). */
+export const TechOrderConfirmationEmail = ({
+ companyName,
+ url,
+}: TechOrderConfirmationEmailProps) => {
+ return (
+
+
+
+
+
+
+
+
+ Your {companyName} ring order #1234567890 is confirmed
+
+
+
+
+
+
+
+
+
+
+
+ Your order has been placed
+
+
+ Order #1234567890 is locked in—we're prepping your
+ Halo rings for shipment and will email the moment they
+ leave our warehouse.
+
+
+ Tracking lands in your inbox as soon as the carrier scans
+ the package.
+
+
+
+
+ {'Track your order \u2192'}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Halo Ring 1
+
+
+ x1
+
+
+
+
+
+
+
+
+
+ Subtotal
+
+
+
+
+ $198.00
+
+
+
+
+
+
+ Tax
+
+
+
+
+ $16.00
+
+
+
+
+
+
+ Shipping
+
+
+
+
+ $0.00
+
+
+
+
+
+
+ Total
+
+
+
+
+ $214.00
+
+
+
+
+
+
+
+
+
+
+
+
+ {companyName} is the AI ring on your finger—easy shopping,
+ clear shipping, and real support when you need it.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 123 Market Street, Floor 1
+
+ Tech City, CA, 94102
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+ );
+};
+
+TechOrderConfirmationEmail.PreviewProps = {
+ companyName: 'Halo',
+ url: 'https://example.com/',
+} satisfies TechOrderConfirmationEmailProps;
+
+export default TechOrderConfirmationEmail;
diff --git a/apps/demo/emails/05-Studio/order-shipping.tsx b/apps/demo/emails/05-Studio/order-shipping.tsx
new file mode 100644
index 0000000000..9f582d4fbe
--- /dev/null
+++ b/apps/demo/emails/05-Studio/order-shipping.tsx
@@ -0,0 +1,260 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Button,
+ Column,
+ Container,
+ Head,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { TechFonts } from './tech-fonts';
+import { techTailwindConfig } from './theme';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+type ShippingFaqItem = {
+ title: string;
+ body: string;
+};
+
+interface TechOrderShippingEmailProps {
+ companyName: string;
+ url: string;
+}
+
+const techOrderShippingFaqItems: ShippingFaqItem[] = [
+ {
+ title: 'When will my ring arrive?',
+ body: 'Most domestic Halo orders land in 3–5 business days after the carrier scan—watch your tracking link for live updates.',
+ },
+ {
+ title: 'Need a different size?',
+ body: 'Start an exchange from your order page within 30 days. We’ll help you resize or swap finishes while stock allows.',
+ },
+ {
+ title: 'Is my shipment protected?',
+ body: 'Every ring ships insured with signature options in select regions—check tracking for delivery preferences.',
+ },
+ {
+ title: 'Can I change the address?',
+ body: 'Contact support fast if the package hasn’t left our hub; once it’s moving, carriers require you to redirect with them.',
+ },
+];
+
+/** Figma Email-Templates `2738:4169` — Tech shipping notification (track CTA, line items, FAQ strip). */
+export const TechOrderShippingEmail = ({
+ companyName,
+ url,
+}: TechOrderShippingEmailProps) => {
+ return (
+
+
+
+
+
+
+
+
+ Your {companyName} ring order #1234567890 has shipped
+
+
+
+
+
+
+
+
+
+
+
+ Your order has shipped
+
+
+ Order #1234567890 with your Halo ring is on the way. Tap
+ track below—we'll ping you if delivery details shift.
+
+
+
+
+ {'Track your order \u2192'}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Halo Ring 1
+
+
+ x1
+
+
+
+
+
+
+
+
+ After dispatch we can’t edit items, but you can follow
+ or reroute delivery through the carrier’s tools anytime.
+
+
+
+
+
+
+ Common questions
+
+
+ {techOrderShippingFaqItems.map((item, idx) => (
+ 0 ? 'mt-10' : ''}>
+
+ {item.title}
+
+
+ {item.body}
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ {companyName} is the AI ring on your finger—easy shopping,
+ clear shipping, and real support when you need it.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 123 Market Street, Floor 1
+
+ Tech City, CA, 94102
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+ );
+};
+
+TechOrderShippingEmail.PreviewProps = {
+ companyName: 'Halo',
+ url: 'https://example.com/',
+} satisfies TechOrderShippingEmailProps;
+
+export default TechOrderShippingEmail;
diff --git a/apps/demo/emails/05-Studio/password-reset.tsx b/apps/demo/emails/05-Studio/password-reset.tsx
new file mode 100644
index 0000000000..a7aacb79e1
--- /dev/null
+++ b/apps/demo/emails/05-Studio/password-reset.tsx
@@ -0,0 +1,156 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Button,
+ Column,
+ Container,
+ Head,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { TechFonts } from './tech-fonts';
+import { techTailwindConfig } from './theme';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+interface TechPasswordResetEmailProps {
+ companyName: string;
+ url: string;
+}
+
+export const TechPasswordResetEmail = ({
+ companyName,
+ url,
+}: TechPasswordResetEmailProps) => (
+
+
+
+
+
+
+
+ Reset your password
+
+
+
+
+
+
+
+
+
+ Reset your password
+
+
+ We got a request to reset your {companyName} store password.
+
+ If that wasn't you, ignore this—your ring orders stay
+ secure.
+
+
+
+
+ Create New Password
+
+
+
+
+
+
+
+ {companyName} is the AI ring on your finger—easy shopping, clear
+ shipping, and real support when you need it.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 123 Market Street, Floor 1
+
+ Tech City, CA, 94102
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+);
+
+TechPasswordResetEmail.PreviewProps = {
+ companyName: 'Halo',
+ url: 'https://example.com/',
+} satisfies TechPasswordResetEmailProps;
+
+export default TechPasswordResetEmail;
diff --git a/apps/demo/emails/05-Studio/promo.tsx b/apps/demo/emails/05-Studio/promo.tsx
new file mode 100644
index 0000000000..136305adbf
--- /dev/null
+++ b/apps/demo/emails/05-Studio/promo.tsx
@@ -0,0 +1,230 @@
+// Get the full source code, including the theme and Tailwind config:
+// https://github.com/resend/react-email/tree/canary/apps/demo/emails
+
+import {
+ Body,
+ Button,
+ Column,
+ Container,
+ Head,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from 'react-email';
+import { TechFonts } from './tech-fonts';
+import { techTailwindConfig } from './theme';
+
+const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : '';
+
+interface TechPromoEmailProps {
+ companyName: string;
+ url: string;
+}
+
+type BenefitItem = {
+ title: string;
+};
+
+const techPromoBenefits: BenefitItem[] = [
+ { title: 'Try your ring risk-free 30 days' },
+ { title: 'Free shipping and easy returns' },
+ { title: 'Two-year ring warranty included' },
+];
+
+/** Figma Email-Templates `2743:1053` — Tech promo (hero, code + redeem, features, join). */
+export const TechPromoEmail = ({ companyName, url }: TechPromoEmailProps) => {
+ return (
+
+
+
+
+
+
+
+ Your {companyName} promo code
+
+
+
+ Black Friday Sale: Save up to $80 on Halo
+
+
+
+
+
+
+
+
+ Save on the ring that keeps you clear
+
+
+
+
+
+
+
+
+
+ Save up to $80
+
+
+ Limited time only
+
+
+
+
+
+
+
+
+ ★★★★★
+
+ “It's wild how fast Halo on my finger felt like
+ calm help whenever my head was full and my hands were
+ busy.”
+
+
+
+
+
+ {techPromoBenefits.map((item, idx) => (
+
+
+ {item.title}
+
+
+ ))}
+
+
+
+
+
+
+ Shop Black Friday Sale
+
+
+
+
+
+
+ {companyName} is the AI ring on your finger—easy shopping,
+ clear shipping, and real support when you need it.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 123 Market Street, Floor 1
+
+ Tech City, CA, 94102
+
+
+
+ Unsubscribe
+ {' '}
+ from {companyName} marketing emails.
+
+
+
+
+
+
+
+ );
+};
+
+TechPromoEmail.PreviewProps = {
+ companyName: 'Halo',
+ url: 'https://example.com/',
+} satisfies TechPromoEmailProps;
+
+export default TechPromoEmail;
diff --git a/apps/demo/emails/05-Studio/tech-fonts.tsx b/apps/demo/emails/05-Studio/tech-fonts.tsx
new file mode 100644
index 0000000000..b9b979de15
--- /dev/null
+++ b/apps/demo/emails/05-Studio/tech-fonts.tsx
@@ -0,0 +1,87 @@
+import { Font } from 'react-email';
+
+const geistLatinWoff2 =
+ 'https://fonts.gstatic.com/s/geist/v4/gyByhwUxId8gMEwcGFU.woff2';
+
+/** Geist + Inter via `react-email` (avoids `}
+
+ {previewText && previewText !== '' && (
+ {previewText}
+ )}
+
+ {children}
+