English
Types Reference
@templatical/types provides all shared TypeScript types, block factory functions, and utilities.
bash
npm install @templatical/typesbash
pnpm add @templatical/typesbash
yarn add @templatical/typesbash
bun add @templatical/typesTemplate Structure
TemplateContent
The root type representing an email template.
ts
interface TemplateContent {
blocks: Block[];
settings: TemplateSettings;
}TemplateSettings
ts
interface TemplateSettings {
width: number; // Template width in pixels (default: 600)
backgroundColor: string; // Background color
textColor: string; // Default text color; text blocks inherit it (default #1a1a1a)
linkColor?: string; // Link color; links inherit the text color when unset
linkUnderline: boolean; // Underline body links (default true)
fontFamily: string; // Default font family
preheaderText?: string; // Email preheader text
locale: string; // BCP-47 content language; <html lang> (default 'en')
direction?: ContentDirection; // 'ltr' | 'rtl'; unset follows the content language
}
type ContentDirection = 'ltr' | 'rtl';Block
A discriminated union of all block types:
ts
type Block =
| TitleBlock
| ParagraphBlock
| ImageBlock
| ButtonBlock
| SectionBlock
| DividerBlock
| VideoBlock
| SpacerBlock
| SocialIconsBlock
| MenuBlock
| TableBlock
| HtmlBlock
| CountdownBlock
| CustomBlock
| SlotBlock
| WrapperBlock;BlockType
ts
type BlockType =
| 'title' | 'paragraph' | 'image' | 'button' | 'section'
| 'divider' | 'video' | 'spacer' | 'social'
| 'menu' | 'table' | 'html' | 'countdown' | 'custom'
| 'slot' | 'wrapper';Base Types
BaseBlock
All blocks extend this interface.
ts
interface BaseBlock {
id: string;
type: string;
styles: BlockStyles;
visibility?: BlockVisibility;
displayCondition?: DisplayCondition;
}BlockStyles
ts
interface BlockStyles {
padding: SpacingValue;
backgroundColor?: string;
}SpacingValue
ts
interface SpacingValue {
top: number;
right: number;
bottom: number;
left: number;
}BorderValue
A border described per side, like SpacingValue: each side has its own width, style and color, and a width of 0 leaves that side undrawn. Sections, images and buttons accept one.
ts
interface BorderSideValue {
width: number; // px; 0 = side not drawn
style: 'solid' | 'dashed' | 'dotted';
color: string;
}
interface BorderValue {
top: BorderSideValue;
right: BorderSideValue;
bottom: BorderSideValue;
left: BorderSideValue;
}BorderRadiusValue
A corner radius in px: one number for all corners, or one per corner. Sections, section wrappers, images and buttons accept it.
ts
type BorderRadiusValue =
| number
| { topLeft: number; topRight: number; bottomRight: number; bottomLeft: number };BlockVisibility
Controls on which viewports a block is visible.
ts
interface BlockVisibility {
desktop: boolean;
mobile: boolean;
}Block Types
See Blocks Guide for detailed descriptions of each block type.
TitleBlock
ts
interface TitleBlock extends BaseBlock {
type: 'title';
content: string;
level: HeadingLevel;
color?: string;
textAlign: 'left' | 'center' | 'right';
fontFamily?: string;
}
type HeadingLevel = 1 | 2 | 3 | 4;ParagraphBlock
ts
interface ParagraphBlock extends BaseBlock {
type: 'paragraph';
content: string;
paragraphSpacing?: number;
}ImageBlock
ts
interface ImageBlock extends BaseBlock {
type: 'image';
src: string;
alt: string;
width: number | 'full';
/** Absent derives the height from the width, keeping the aspect ratio. */
height?: number;
/** The image file's own size, for the aspect-ratio lock. Never rendered. */
naturalWidth?: number;
naturalHeight?: number;
align: 'left' | 'center' | 'right';
/** Corner radius in px. Omitted/0 = square corners. */
borderRadius?: BorderRadiusValue;
/** Omitted = no border. */
border?: BorderValue;
linkUrl?: string;
linkOpenInNewTab?: boolean;
placeholderUrl?: string;
decorative?: boolean;
}ButtonBlock
ts
interface ButtonBlock extends BaseBlock {
type: 'button';
text: string;
url: string;
backgroundColor: string;
textColor: string;
borderRadius: BorderRadiusValue;
/** Outline button: backgroundColor 'transparent' (the keyword), plus a textColor. An empty fill exports as that keyword. */
border?: BorderValue;
fontSize: number;
buttonPadding: SpacingValue;
fontFamily?: string;
openInNewTab?: boolean;
width?: number | 'full';
align: 'left' | 'center' | 'right';
}SectionBlock
Container for multi-column layouts.
ts
interface SectionBlock extends BaseBlock {
type: 'section';
columns: ColumnLayout;
children: Block[][];
stackOnMobile?: boolean;
borderRadius?: BorderRadiusValue;
border?: BorderValue;
backgroundImage?: BackgroundImage;
hero?: SectionHero;
wrapper?: SectionWrapper;
}
interface SectionWrapper {
backgroundColor?: string;
padding?: SpacingValue;
borderRadius?: BorderRadiusValue;
backgroundImage?: BackgroundImage;
}
type ColumnLayout = '1' | '2' | '3' | '2-1' | '1-2';
interface BackgroundImage {
url: string;
size: 'cover' | 'contain' | 'auto';
position: {
x: 'left' | 'center' | 'right';
y: 'top' | 'center' | 'bottom';
};
repeat: boolean; // false: painted once
naturalWidth?: number; // the image file's own size in px; a hero's 'image' height keeps this shape
naturalHeight?: number;
}
interface SectionHero {
height: 'image' | number; // 'image': as tall as the image across the desktop box; a number: fixed px, padding included
verticalAlign: 'top' | 'middle' | 'bottom';
}DividerBlock
ts
/** "0%" to "100%" */
type DividerPercentWidth = `${number}%`;
interface DividerBlock extends BaseBlock {
type: 'divider';
lineStyle: 'solid' | 'dashed' | 'dotted';
color: string;
thickness: number;
width: number | 'full' | DividerPercentWidth;
}VideoBlock
Renders as a linked thumbnail image -- email clients do not support embedded playback.
ts
interface VideoBlock extends BaseBlock {
type: 'video';
url: string;
openInNewTab?: boolean;
thumbnailUrl: string; // Auto-derived from a YouTube/Vimeo url when empty
alt: string;
width: number | 'full';
/** Absent derives the height from the width, keeping the aspect ratio. */
height?: number;
align: 'left' | 'center' | 'right';
placeholderUrl?: string;
}SpacerBlock
ts
interface SpacerBlock extends BaseBlock {
type: 'spacer';
height: number;
}SocialIconsBlock
ts
interface SocialIconsBlock extends BaseBlock {
type: 'social';
icons: SocialIcon[];
iconStyle: SocialIconStyle;
iconTone?: SocialIconTone; // absent means 'brand'
iconSize: SocialIconSize;
spacing: number;
align: 'left' | 'center' | 'right';
}
interface SocialIcon {
id: string;
platform: SocialPlatform;
url: string;
}
type SocialPlatform =
| 'facebook' | 'twitter' | 'instagram' | 'linkedin'
| 'youtube' | 'tiktok' | 'pinterest' | 'email'
| 'whatsapp' | 'telegram' | 'discord' | 'snapchat'
| 'reddit' | 'github' | 'dribbble' | 'behance'
| 'website';
type SocialIconStyle =
| 'solid' | 'outlined' | 'rounded' | 'square' | 'circle' | 'plain';
type SocialIconTone = 'brand' | 'dark' | 'light';
type SocialIconSize = 'small' | 'medium' | 'large';MenuBlock
ts
interface MenuBlock extends BaseBlock {
type: 'menu';
items: MenuItemData[];
fontSize: number;
fontFamily?: string;
color: string;
linkColor?: string;
textAlign: 'left' | 'center' | 'right';
separator: string;
separatorColor: string;
spacing: number;
}
interface MenuItemData {
id: string;
text: string;
url: string;
openInNewTab: boolean;
bold: boolean;
underline: boolean;
color?: string;
}TableBlock
ts
interface TableBlock extends BaseBlock {
type: 'table';
rows: TableRowData[];
hasHeaderRow: boolean;
headerBackgroundColor?: string;
borderColor: string;
borderWidth: number;
cellPadding: number;
fontSize: number;
fontFamily?: string;
color: string;
textAlign: 'left' | 'center' | 'right';
}
interface TableRowData {
id: string;
cells: TableCellData[];
}
interface TableCellData {
id: string;
content: string;
}HtmlBlock
ts
interface HtmlBlock extends BaseBlock {
type: 'html';
content: string; // Raw HTML
}CountdownBlock
Renders as an animated GIF, which requires the Templatical Cloud backend -- @templatical/renderer emits a templatical:unrenderable-block marker instead. See Blocks with no renderer.
ts
interface CountdownBlock extends BaseBlock {
type: 'countdown';
targetDate: string; // Local date/time, 'YYYY-MM-DDTHH:mm'
timezone: string; // IANA name the target is read in, e.g. 'Europe/Berlin'
showDays: boolean;
showHours: boolean;
showMinutes: boolean;
showSeconds: boolean;
separator: ':' | '-' | ' ';
digitFontSize: number;
digitColor: string;
labelColor: string;
labelFontSize: number;
backgroundColor: string;
fontFamily?: string;
labelDays: string;
labelHours: string;
labelMinutes: string;
labelSeconds: string;
expiredMessage: string;
expiredImageUrl: string; // Shown instead of the timer once expired
hideOnExpiry: boolean;
}CustomBlock
ts
interface CustomBlock extends BaseBlock {
type: 'custom';
customType: string;
fieldValues: Record<string, unknown>;
renderedHtml?: string;
renderedStylesheet?: string;
dataSourceFetched?: boolean;
}
interface SlotBlock extends BaseBlock {
type: 'slot';
}
interface WrapperBlock extends BaseBlock {
type: 'wrapper';
children: Block[];
borderRadius?: number;
backgroundImage?: BackgroundImage;
}slot and wrapper are layout markers. The skill never emits them. See Layout.
Configuration Types
MergeTag
ts
interface MergeTag {
label: string;
value: string;
group?: string; // picker-only grouping label
description?: string; // picker-only helper text
sample?: string; // preview-only example value; never written to MJML
}
interface MergeTagRequestContext {
reason: 'insert' | 'edit';
current?: MergeTag; // on edit, when the token matches tags
}MergeTagsConfig
ts
interface MergeTagsConfig {
syntax?: SyntaxPresetName | SyntaxPreset;
tags?: MergeTag[];
onRequest?: (context?: MergeTagRequestContext) => Promise<MergeTag | null>;
showRawValue?: boolean; // reveal raw token in tooltip (default true)
autocomplete?: boolean; // typing-based autocomplete (default true)
}
type SyntaxPresetName = 'liquid' | 'handlebars' | 'mailchimp' | 'ampscript';DisplayCondition
ts
interface DisplayCondition {
label: string;
before: string;
after: string;
group?: string;
description?: string;
}DisplayConditionsConfig
ts
interface DisplayConditionsConfig {
conditions: DisplayCondition[];
allowCustom?: boolean;
}CustomBlockDefinition
ts
interface CustomBlockDefinition {
type: string;
name: string;
icon?: string;
description?: string;
fields: CustomBlockField[];
template: string; // Liquid template
dataSource?: DataSourceConfig;
defaultStyles?: Partial<BlockStyles>; // styles applied to new instances
stylesheet?: string; // CSS emitted once into mj-head
}See Custom Blocks for field type details.
ThemeOverrides
ts
interface ThemeOverrides {
bg?: string;
bgElevated?: string;
bgHover?: string;
bgActive?: string;
border?: string;
borderLight?: string;
text?: string;
textMuted?: string;
textDim?: string;
primary?: string;
primaryHover?: string;
primaryLight?: string;
secondary?: string;
secondaryHover?: string;
secondaryLight?: string;
success?: string;
successLight?: string;
warning?: string;
warningLight?: string;
danger?: string;
dangerLight?: string;
canvasBg?: string;
dark?: Omit<ThemeOverrides, 'dark'>;
}FontsConfig
ts
interface FontsConfig {
defaultFallback?: string;
defaultFont?: string;
customFonts?: CustomFont[];
builtIns?: boolean | string[]; // true/omitted: all seven; false: none; string[]: allowlist — see /guide/fonts
}
interface CustomFont {
name: string;
url: string;
fallback?: string;
}ViewportSize
ts
type ViewportSize = 'desktop' | 'mobile';Factory Functions
The block factory functions below accept an optional partial override object and return a complete block with a generated ID. createCustomBlockLiquid(liquidjs) is also exported for headless custom-block rendering; see the Custom blocks guide for its contract and optional liquidjs peer.
The factory's public module shape has no declaration dependency on liquidjs. Passing the actual module preserves its Liquid instance type:
ts
export interface CustomBlockLiquidModule {
Liquid: new (...args: any[]) => any;
Tag: abstract new (...args: any[]) => any;
CycleTag: new (...args: any[]) => any;
Value: new (...args: any[]) => any;
toValue: (value: any) => any;
}
export declare function createCustomBlockLiquid<M extends CustomBlockLiquidModule>(module: M): InstanceType<M['Liquid']>;ts
import {
createTitleBlock,
createParagraphBlock,
createImageBlock,
createButtonBlock,
createSectionBlock,
createBackgroundImage,
createDividerBlock,
createVideoBlock,
createSpacerBlock,
createSocialIconsBlock,
createMenuBlock,
createTableBlock,
createHtmlBlock,
createCountdownBlock,
createCustomBlock,
createBlock,
cloneBlock,
createDefaultTemplateContent,
generateId,
} from '@templatical/types';
// Countdown GIFs need Cloud or a `blockRenderers.countdown` override; the OSS renderer emits a placeholder.
// Create with defaults
const paragraph = createParagraphBlock();
// Create with overrides
const heading = createTitleBlock({
content: '<h1>Hello</h1>',
level: 1,
});
// Create any block by type string
const block = createBlock('button');
// A section background image from its URL: centered, cover, not tiled
const section = createSectionBlock({
backgroundImage: createBackgroundImage('https://cdn.example.com/hero.jpg'),
});
// Deep clone with new IDs, nested rows, items and icons included
const copy = cloneBlock(existingBlock);
// Empty template
const template = createDefaultTemplateContent();
// Generate a UUID
const id = generateId();Type Guards
ts
import {
isTitle, isParagraph, isImage, isButton, isSection,
isDivider, isVideo, isSpacer, isSocialIcons,
isMenu, isTable, isHtml, isCountdown, isCustomBlock,
} from '@templatical/types';
if (isTitle(block)) {
console.log(block.level); // TypeScript knows this is TitleBlock
}EventEmitter
A typed event emitter for subscription patterns.
ts
import { EventEmitter } from '@templatical/types';
type Events = {
change: TemplateContent;
select: string | null;
};
const emitter = new EventEmitter<Events>();
const unsubscribe = emitter.on('change', (content) => {
console.log('Changed:', content);
});
emitter.emit('change', templateContent);
unsubscribe(); // Remove listenerMethods
| Method | Description |
|---|---|
on(event, handler) | Subscribe. Returns unsubscribe function |
off(event, handler) | Unsubscribe a specific handler |
emit(event, data) | Emit an event |
removeAllListeners(event?) | Remove all listeners, optionally for a specific event |
listenerCount(event) | Number of listeners for an event |
Storage contracts
Template, TemplatePatch, SavedBlockInput, CommentsProvider, MediaProvider and the rest of the BYO storage shapes live under Connect your backend. This page is the block model and editor config.
Operations and collaboration
Operation and its six members, BlockParent, OperationErrorCode, OperationError, applyOperations, applyOperationsInPlace and getColumnCount are described in the Operations reference. The collaboration contract — CollaborationProvider, CollaborationHandlers, OperationBatch, RevisionedBatch, CollaborationActor, CollaborationMember, BlockLock, BlockLockHolder, CollaborationConflict and CollaborationRemoteChange — is on Collaboration. Template carries revision and locks for a collaborating store.
AI
AiProvider, AiConversationProvider, the AI requests, handlers, results, AiChatMessage and AiMergeTags form the MIT provider contract in @templatical/types. AI describes the contract; the AI API reference covers the HTTP client, server kit and wire.
SdkError(message, statusCode?, code?, details?) carries AI error codes and optional details. ThrottledError extends it with retryAfterMs, status 429 and code rate_limited. invertOperations(content, ops) returns a batch's guarded inverse; it lives in @templatical/types and is re-exported by @templatical/core.