Skip to content

Programmatic Templates ​

Most users will design templates visually in the drag-and-drop editor. But sometimes you need to create templates in code -- seeding default content, building templates from API data, or generating emails programmatically on the server.

Templatical provides factory functions for every block type. Use them to build templates in code, then pass them to init() or render them directly with the renderer.

Blank template ​

ts
import { createDefaultTemplateContent } from '@templatical/types';

const content = createDefaultTemplateContent();
// { blocks: [], settings: { width: 600, backgroundColor: '#ffffff',
//   textColor: '#1a1a1a', linkUnderline: true, fontFamily: 'Arial', locale: 'en' } }

createDefaultTemplateContent() accepts an optional font family string:

ts
const content = createDefaultTemplateContent('Georgia, serif');

Building a template ​

Every block type has a corresponding create*Block() function. Each accepts an optional partial object to override defaults. All factory functions auto-generate a unique id for each block.

ts
import {
  createDefaultTemplateContent,
  createTitleBlock,
  createParagraphBlock,
  createImageBlock,
  createButtonBlock,
  createDividerBlock,
} from '@templatical/types';

const content = createDefaultTemplateContent();

content.blocks = [
  createTitleBlock({
    content: '<h1 style="text-align: center;">Welcome aboard</h1>',
    level: 1,
  }),
  createImageBlock({
    src: 'https://example.com/hero.jpg',
    alt: 'Welcome hero image',
    width: 'full',
  }),
  createDividerBlock(),
  createParagraphBlock({
    content: '<p>Thanks for signing up. Here is what happens next.</p>',
  }),
  createButtonBlock({
    text: 'Get Started',
    url: 'https://example.com/dashboard',
    backgroundColor: '#1a73e8',
    textColor: '#ffffff',
    borderRadius: 6,
  }),
];

Block factory reference ​

Title ​

ts
createTitleBlock({
  content: '<h1>Welcome, {{name}}!</h1>',
  level: 1,
  textAlign: 'center',
})

Paragraph ​

ts
createParagraphBlock({
  content: '<p>Thanks for signing up. Here is what happens next.</p>',
})

Image ​

ts
createImageBlock({
  src: 'https://cdn.example.com/hero.png',
  alt: 'Hero banner',
  width: 560,
  linkUrl: 'https://example.com',
})

Button ​

ts
createButtonBlock({
  text: 'Get Started',
  url: 'https://example.com/signup',
  backgroundColor: '#6366f1',
  borderRadius: 8,
})

Divider ​

ts
createDividerBlock({
  lineStyle: 'dashed',
  color: '#e5e7eb',
  thickness: 2,
})

Spacer ​

ts
createSpacerBlock({ height: 40 })

HTML ​

ts
createHtmlBlock({
  content: '<div style="text-align:center;">Custom markup</div>',
})

Social Icons ​

ts
createSocialIconsBlock({
  iconStyle: 'circle',
  iconSize: 'large',
  icons: [
    { id: crypto.randomUUID(), platform: 'twitter', url: 'https://x.com/acme' },
    { id: crypto.randomUUID(), platform: 'github', url: 'https://github.com/acme' },
  ],
})
ts
createMenuBlock({
  items: [
    { id: crypto.randomUUID(), text: 'Home', url: 'https://example.com', openInNewTab: false, bold: false, underline: false },
    { id: crypto.randomUUID(), text: 'Blog', url: 'https://example.com/blog', openInNewTab: false, bold: false, underline: false },
    { id: crypto.randomUUID(), text: 'Docs', url: 'https://docs.example.com', openInNewTab: true, bold: false, underline: false },
  ],
  separator: '-',
})

Table ​

ts
createTableBlock({
  hasHeaderRow: true,
  rows: [
    { id: crypto.randomUUID(), cells: [{ id: crypto.randomUUID(), content: 'Plan' }, { id: crypto.randomUUID(), content: 'Price' }] },
    { id: crypto.randomUUID(), cells: [{ id: crypto.randomUUID(), content: 'Starter' }, { id: crypto.randomUUID(), content: '$9/mo' }] },
    { id: crypto.randomUUID(), cells: [{ id: crypto.randomUUID(), content: 'Pro' }, { id: crypto.randomUUID(), content: '$29/mo' }] },
  ],
})

Video ​

ts
createVideoBlock({
  url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
  thumbnailUrl: 'https://img.youtube.com/vi/dQw4w9WgXcQ/maxresdefault.jpg',
  alt: 'Product demo video',
})

Countdown ​

createCountdownBlock builds a countdown block, but countdown GIFs need Cloud or a blockRenderers.countdown override — the OSS renderer emits a placeholder.

Section ​

ts
createSectionBlock({
  columns: '2',
  children: [
    [createParagraphBlock({ content: '<p>Left column</p>' })],
    [createImageBlock({ src: 'https://cdn.example.com/photo.jpg' })],
  ],
})

The columns property accepts: '1' (single), '2' (two equal), '3' (three equal), '2-1' (two-thirds / one-third), '1-2' (one-third / two-thirds). See Sections and Columns for full details.

Custom ​

createCustomBlock takes a CustomBlockDefinition (not a partial block). It generates field values from the definition's field defaults. See Custom Blocks for defining custom block types.

Utilities ​

Generic factory ​

Create any block by type string:

ts
import { createBlock } from '@templatical/types';

const block = createBlock('title'); // TitleBlock with defaults

Cloning ​

Deep-clone a block with a new ID:

ts
import { cloneBlock } from '@templatical/types';

const copy = cloneBlock(existingBlock);
// copy.id !== existingBlock.id

Type guards ​

Narrow a Block union to a specific type:

ts
import { isTitle, isParagraph, isImage, isButton, isSection } from '@templatical/types';

if (isTitle(block)) {
  console.log(block.level); // TypeScript knows this is TitleBlock
}

if (isParagraph(block)) {
  console.log(block.content); // TypeScript knows this is ParagraphBlock
}

if (isImage(block)) {
  console.log(block.src);
}

Every block type has a corresponding guard: isTitle(), isParagraph(), isImage(), isButton(), isDivider(), isSpacer(), isHtml(), isSocialIcons(), isMenu(), isTable(), isVideo(), isSection(), isCountdown(), isCustomBlock().

Template settings ​

Template settings control the global properties of the email:

ts
const content = createDefaultTemplateContent();

content.settings.width = 640;
content.settings.backgroundColor = '#f5f5f5';
content.settings.fontFamily = 'Helvetica, Arial, sans-serif';
content.settings.preheaderText = 'Your weekly digest is here';
content.settings.locale = 'en';
content.settings.direction = 'ltr';
SettingTypeDescription
widthnumberEmail width in pixels
backgroundColorstringOuter background color
textColorstringDefault text color inherited by all text blocks; per-block colors override. Defaults to #1a1a1a
linkColorstringDocument link color applied to every link (rich-text and menu); links inherit the text color when unset. Optional
linkUnderlinebooleanUnderline body (rich-text) links. Defaults to true
fontFamilystringDefault font stack
preheaderTextstringPreview text shown in inbox list
localestringBCP-47 content language (<html lang>)
direction'ltr' | 'rtl'Writing direction. Unset follows the content language

For default values and how to customize them, see Block & Template Defaults.

Loading saved content ​

Pass previously saved JSON back to the editor:

ts
const res = await fetch('/api/templates/123');
const saved = await res.json();

const editor = await init({
  container: '#editor',
  content: saved,
});

You can also update content after initialization:

ts
editor.setContent(newContent);

Next steps ​