Skip to content

Types Reference ​

@templatical/types provides all shared TypeScript types, block factory functions, and utilities.

bash
npm install @templatical/types
bash
pnpm add @templatical/types
bash
yarn add @templatical/types
bash
bun add @templatical/types

Template 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';
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 listener

Methods ​

MethodDescription
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.