Brands

Give every template your organization's colors, fonts, logo, and footer by importing a brand, and republish when the brand changes.

A brand is how your organization's email looks and signs off. It has three parts:

  • Theme: the colors, fonts, text sizes, and corners templates use, written as CSS theme variables.
  • Logo and icon: a light logo, a dark logo, and a square icon, each an https URL.
  • Footer: your company name, postal address, and social links.

Your organization has one default brand and can hold up to 12 brands, each with its own slug, for example one per product. The default brand cannot be deleted; make another brand the default first.

Manage brands in the dashboard

Open Templates and choose the Brands tab. A brand page has four sections:

  • Details: the name and slug, and whether it is the default.
  • Theme: a Form for colors, fonts, and corners, and a CSS tab over the same text. The form is locked while the CSS has errors.
  • Logo and icon: the light and dark logos and the icon.
  • Footer: company name, postal address, and social links.

The font picker in the theme form offers Google fonts or your own WOFF2 files. Samva hosts either on its own asset origin and writes the @font-face rules into the brand's CSS, so recipients never load fonts from Google.

Import a brand in a template

A template uses a brand only when its project theme, theme.css, imports it:

theme.css
@import "./starter.css";
@import "samva:brand";

@theme {
  --radius-button: 999px;
}

The order is the precedence, lowest first: the starter the project was created from, then the brand, then your project's own @theme overrides. @import "samva:brand"; names your default brand; @import "samva:brand/<slug>"; names one brand. A project imports at most one brand. A project without the import is unbranded. An @import of anything other than a project .css file or samva:brand fails the check.

Creating a template from a starter in the dashboard offers a Use my brand checkbox, which writes the import line for you.

Utilities read the brand's theme variables like any other: bg-brand, text-brand-foreground, font-heading, rounded-button. Starters use these names, so a brand restyles every starter:

Theme variableUsed for
--color-brandThe primary color: buttons and the main call to action.
--color-brand-foregroundText on the primary color.
--color-accentEyebrows, labels, links, and highlights.
--color-surfaceThe content card.
--font-headingHeadings.
--font-bodyBody text.
--radius-buttonButton corners.
--radius-cardThe content card's corners.

When a brand sets --color-brand and leaves out --color-brand-foreground, the foreground is black or white, whichever contrasts more with the brand color.

samva:brand also exports two components bound to the brand the project imports:

emails/welcome.tsx
/** @jsxImportSource @samva/markup/email */
import { Schema } from "effect";
import { Email, Section } from "@samva/markup/email/components";
import { defineEmail } from "@samva/markup/template";
import { BrandFooter, BrandLogo } from "samva:brand";

export default defineEmail({
  id: "welcome",
  schema: Schema.toStandardJSONSchemaV1(Schema.Struct({ firstName: Schema.String })),
  fixtures: { basic: { firstName: "Ada" } },
  render: (input) => ({
    subject: `Welcome, ${input.firstName}`,
    body: (
      <Email>
        <Section>
          <BrandLogo width={120} align="left" />
          <h1 className="font-heading text-brand">Welcome, {input.firstName}</h1>
        </Section>
        <BrandFooter unsubscribeLabel="Unsubscribe" />
      </Email>
    ),
  }),
});
  • <BrandLogo /> renders the light logo and swaps to the dark logo in clients that support dark mode. It renders nothing when the brand has no logo. alt defaults to the company name.
  • <BrandFooter /> renders the company name, postal address, social links, and an unsubscribe link. The unsubscribe row appears only when the send has an unsubscribe group, and Samva fills its link at send time.

For tsc, add "types": ["@samva/markup/samva-brand"] to the project's tsconfig.json.

Write a brand theme

A brand's CSS holds only theme variables and fonts:

brand.css
@font-face {
  font-family: "Acme Sans";
  src: url("https://fonts.example.com/acme-sans-400.woff2") format("woff2");
  font-weight: 400;
}

@theme {
  --color-brand: #4f46e5;
  --color-accent: #0ea5e9;
  --font-heading: "Acme Sans", Helvetica, Arial, sans-serif;
  --radius-button: 8px;
}

@media (prefers-color-scheme: dark) {
  @theme {
    --color-brand: #818cf8;
  }
}
  • @theme blocks set theme variables in Tailwind's namespaces: --color-*, --font-*, --text-*, --leading-*, --tracking-*, --radius-*, --spacing, --container-*, and --shadow-*.
  • One dark-mode block, @media (prefers-color-scheme: dark) { @theme { ... } }, sets colors only. Email inlines styles when it is built, so each dark color becomes a -dark variable, and a template reads it with a dark: utility: bg-brand dark:bg-brand-dark.
  • @font-face rules take an https WOFF2 src.

Anything else, such as a selector rule, another at-rule, or a relative font path, is rejected with each finding located in the CSS. A font stack should end in sans-serif, serif, or monospace, because Gmail and classic Outlook do not load webfonts. A font file committed in the template project instead belongs in theme.css; see Images and fonts in a project.

Publish and republish

Each build compiles against the brand as it is at that moment, and a publication keeps a snapshot of it: theme, logos, footer, and digest. Editing a brand never changes what an existing publication sends. Republish to send the edited brand.

In the dashboard, a template whose publication was built against an older version of its brand shows Brand updated since publish. Open it for a preview of the difference and a Republish button. On the brand page, Republish all using the brand republishes every such template and lists any that failed. From the CLI, samva templates publish republishes the same commit against the current brand; with an unchanged brand it returns the existing receipt.

Each domain uses a brand, the default brand unless you choose another under Uses brand on the domain page or with brandSlug in Update a domain's send-settings config. The domain's brand supplies only the footer Samva appends to a message whose template does not render <BrandFooter />. It never restyles a template. A template that renders <BrandFooter /> signs off with the brand it was published with, even when that differs from the domain's brand, and the dashboard points out the difference without failing any send.

Campaigns

Every campaign signs off with a brand footer that includes a postal address and an unsubscribe link:

  • The appended footer is on for campaigns. Turning it off is allowed only when the campaign's template renders <BrandFooter />; otherwise scheduling and sending fail with CampaignFooterRequiredError.
  • The brand that signs the campaign off must have a postal address, or scheduling and sending fail with BrandAddressRequiredError.

Transactional sends never fail on either rule.

Work with brands from code

The brands API lists, creates, reads, updates, and deletes brands under /v1/brands and /v1/brands/{slug}, and makes one the default. An update replaces each section it includes (css, assets, or footer) and leaves the others unchanged.

update-brand.ts
import { createClient } from "samva";

const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });
const brand = await samva.brands.get({ slug: "acme" });
console.log(brand.digest);

The CLI pulls and pushes brands; see Pull and push a brand:

samva brands list
samva brands get acme --json
samva brands pull acme
samva brands push brand.css --brand acme

Local previews and samva templates check resolve the imported brand live, using SAMVA_API_KEY or your samva login session. Offline, they use the snapshot samva brands pull wrote to .samva/brands/<slug>.json and warn that it may be out of date. With neither, the check fails with brand-unavailable. Keep .samva in .gitignore.

The hosted MCP server offers brands_list, brands_get, and brands_update. The template editor's assistant reads the brand a project imports and never edits it.

Related documentation

On this page