Unlayer
Content builder

Unlayer Best Practices for Developers: Integration & Usage Guide

Unlayer Best Practices for Developers: Integration & Usage Guide
Share

Embedding Unlayer is straightforward. The real agenda is to make sure that it performs well, scales cleanly, and holds up in production. 

If you are a developer who’s past the initial setup and is focused on getting Unlayer right in your live app, this is the guide you will want to keep in your pocket. 

This guide covers all Unlayer best practices for developers, including how to integrate the builder, optimize performance, manage multiple editor instances and content types, customize and extend functionality, troubleshoot integration issues, and use the developer console effectively.

Let’s dive into them one by one. 

1. Initial Integration & Setup

✅ Choose the right integration method 

  • Use the core JavaScript SDK (unlayer.init) if you are building without a frontend framework.

<script src="https://editor.unlayer.com/embed.js"></script>
  • For framework-specific integration, install the official wrapper from npm or yarn.

1. For React:

npm install @unlayer/react

or

yarn add @unlayer/react

2. For Angular:

npm install @unlayer/angular

or

yarn add @unlayer/angular

3. For Vue:

npm install @unlayer/vue

or

yarn add @unlayer/vue

Why: Wrappers handle mounting, unmounting, or reactivity automatically. The SDK provides full control, but you have to manage the component lifecycle manually.

✅ Always set the projectId when initializing the editor

unlayer.init({
 id: 'editor-container', // ID of the container created in previous step
 projectId: 1234, // Add your project ID here
 displayMode: 'email', // Can be 'email', 'web' or 'popup'
});

Why: Setting the projectId will automatically load your project configuration and entitlements from the Unlayer dashboard, including themes, tools, permissions, and saved appearance settings. Otherwise, you have to configure it all manually in your code.    

✅ Set up builder appearance settings during initialization

appearance: {
  theme: 'light',
  panels: {
    tools: { dock: 'left' }
  }
}


Why: This controls colors, layout, branding, and tool panel behavior, ensuring the builder UI matches your app from the start.

✅ Load custom CSS/JS before unlayer.init()using customCSS and customJS

​​Custom CSS

 Add custom CSS via string or URL

unlayer.init({
  customCSS: 'https://example.com/custom-styles.css',
});

Or, pass CSS source code like this: 

unlayer.init({
  customCSS: [
    "
      body {
        background-color: yellow;
      }
    "
  ]
});

Custom JS

Pass custom JS to the builder using the customJS property during initialization.

unlayer.init({
  customJS: '//cdn.muicss.com/mui-0.9.28/js/mui.min.js',
});

Or, pass the JS source code like this: 

unlayer.init({
  customJS: [
    "
      console.log('I am custom JS!');
    "
  ]
});

Why: Any custom styles or tools must be loaded before the builder initialization; otherwise, it will cause rendering issues. 

📄 Documentation: Custom JS/CSS

✅ Set the displayMode based on the type of your content

unlayer.init({
  id: 'editor-container',
  projectId: 1234,
  displayMode: 'email' // Can be 'email', 'web', or 'popup'
});

Why: The unlayer builder may default to email behavior, which might not be your use case. Set displayMode to optimize the builder according to your content type.

2. Optimizing Editor's Performance 

 ✅ Lazy-load the builder 

const script = document.createElement('script');
script.src = 'https://editor.unlayer.com/embed.js';
script.onload = () => {
  unlayer.init({ id: 'editor', projectId: 1234 });
};
document.head.appendChild(script);

Why: Loading Unlayer on demand will prevent it from blocking your application’s initial load and will keep the bundle size smaller. 

✅ Reduce the initial payload

unlayer.init({
  id: 'editor',
  projectId: 1234,
  tools: {
    image: { enabled: true },
    video: { enabled: false }
  },
  fonts: [],
  design: { body: { rows: [] } }
});

Why: Avoiding loading fonts, tools, or prefilled designs unnecessarily during initialization keeps the builder fast and responsive. 

✅ Debounce save operations

unlayer.init({
 features: {
   textEditor: {
     triggerChangeWhileEditing: true,
     debounce: 100, // Trigger changes every 100ms during editing
   },
 },
});

Why: Avoiding triggering save requests on a small change reduces backend load and keeps the UI fast and responsive.

✅ Avoid unnecessary mounting/unmounting of the builder 

Why: Calling unlayer.init() or unlayer.destroy() repeatedly slows down your app and causes performance issues. Instead, keep the builder mounted and toggle visibility rather than tearing it down.

<!-- HTML -->
<div id="editor-container" style="display: none;"></div>

<button onclick="toggleEditor()">Toggle Editor</button>

<script>
  let isEditorInitialized = false;

  function toggleEditor() {
    const editor = document.getElementById('editor-container');

    if (!isEditorInitialized) {
      unlayer.init({
        id: 'editor-container',
        projectId: 1234
      });
      isEditorInitialized = true;
      editor.style.display = 'block';
    } else {
      // Just toggle visibility instead of destroying
      editor.style.display = editor.style.display === 'none' ? 'block' : 'none';
    }
  }
</script>

✅ Use callbacks wisely

Why: Use callbacks like onLoad, onDesignLoad, or onExport only when necessary and keep the logic lightweight. 

For example, callbacks, especially onExport, contain heavy or blocking logic that can slow down the builder or delay actions. So, keeping them minimal will maintain a smooth and responsive editor experience.

unlayer.init({
  id: 'editor',
  projectId: 1234,
  onExport: function (data) {
    // ✅ Lightweight logic only — non-blocking, async
    fetch('/api/save-html', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ html: data.html })
    }).then(() => {
      console.log('Export saved.');
    });

    // ❌ Avoid: heavy computation or synchronous blocking code here
  }
});

✅ Optimize the usage of assets

Why: Unlayer’s built-in CDN (used with default storage or S3 integration) compresses and optimizes uploaded images for better performance. However, we recommend starting with lightweight assets like compressed images or SVGs to avoid slowdowns during upload, rendering, and loading large templates.

Note: If you're using a custom file storage solution like your own cloud, you must manually handle image uploads.

unlayer.registerCallback('image', function (file, done) {
  var data = new FormData();
  data.append('file', file.attachments[0]);

  fetch('/uploads', {
    method: 'POST',
    headers: {
      Accept: 'application/json',
    },
    body: data,
  })
    .then((response) => {
      if (response.status >= 200 && response.status < 300) {
        return response.json();
      } else {
        throw new Error(response.statusText);
      }
    })
    .then((data) => {
      done({ progress: 100, url: data.filelink });
    })
    .catch((error) => {
      console.error('Upload failed:', error);
    });
});

✅ Lock the builder version

unlayer.init({
 version: '1.63.2', // Lock to a specific version
});

Why: Lock the builder to a specific version to prevent unexpected changes when Unlayer releases any updates and to keep your integration stable.

3. Managing Builder Instances

✅ Use a separate container for each builder instance

unlayer.init({
  id: 'email-builder',
  displayMode: 'email',
});

unlayer.init({
  id: 'page-builder',
  displayMode: 'web',
});

Why: Initializing the builder in its unique DOM element to avoid tool overlaps, broken layouts, and event binding conflicts.

✅ Destroy builder instance before re-initializing

unlayer.destroy(); // Clean up existing instance

unlayer.init({
  id: 'builder-container',
  displayMode: 'web',
});

Why: Whether you're re-initializing the builder or switching between content types, always make sure to destroy the previous instance first. This helps avoid unexpected UI issues, memory leaks, and duplicate event listeners.

✅ Switch builder mode dynamically

function switchBuilder(type) {
  unlayer.destroy(); // Clean up previous instance

  unlayer.init({
    id: 'builder-container',
    projectId: 1234,
    displayMode: type, // 'email' | 'web' | 'popup'
  });
}

Why: Reusing the same container for different content types while switching displayMode prevents DOM clutter, reduces memory usage, and ensures that every builder loads with the right tools and configuration.

✅ Initialize the builder only when the container is visible

function showTabAndInitBuilder() {
  const tab = document.getElementById('builder-tab');
  tab.style.display = 'block'; // Make container visible

  unlayer.init({
    id: 'builder-container',
    projectId: 1234,
    displayMode: 'email',
  });
}

Why: Initializing the builder inside a hidden container may cause the canvas and panels to render incorrectly or break. 

4. Supporting Multiple Content Types

✅ Load pre-configured templates based on content type

function loadTemplateFor(type) {
  const template = getTemplateByType(type); // Your logic to fetch the design JSON
  unlayer.loadDesign(template);
}


 Why: Use the load and save design feature to pull in the right template dynamically. It gives your users a head start with a ready-made layout, saving time and keeping designs consistent.

✅ Configure export endpoints per content type and format

unlayer.exportHtml((data) => {
  const { design, html } = data;
  const exportType = getExportType(); // e.g., 'email', 'page', 'popup'

  saveExport(exportType, {
    html,
    design,
    text: unlayer.exportText(), // Optional plain text
  });
});

Why: Different types of content require different export formats. Web pages usually require only HTML, but emails need both HTML and plain text for compatibility across email clients. Configuring the export logic for every content type ensures seamless integration with downstream systems. 

📄 Documentation: Export Designs 

✅ Restrict builder access with project entitlements

Why: This prevents users from accessing unsupported builder modes, keeps the UI clean, and reduces implementation errors.

Note: Project entitlements are managed via Unlayer’s dashboard. Contact us to enable them.

5. Customization & Extensibility

✅ Register custom tool to add app-specific functionality

unlayer.registerTool({
  name: 'product-listing',
  label: 'Product Listing',
  icon: 'fa-shopping-cart',
  supportedDisplayModes: ['email', 'web'],
  properties: {
    name: {
      label: 'Title',
      defaultValue: 'Featured Products'
    }
  },
  values: {
    name: 'Featured Products'
  },
  render: function () {
    return `<div>{{ dynamicProductHTML }}</div>`;
  }
})

Why: Registering custom tools using unlayer.registerTool() exposes your app’s features (like product listings or widgets) as drag-and-drop blocks inside the builder so that the end-users can add them to their design without writing a single line of code. 

📄 Learn How to Create a Custom Tool ➞

✅ Use custom property editors and advanced options for logic/UI control

unlayer.registerPropertyEditor({
  name: 'custom-color-picker',
  layout: 'bottom',
  Widget: CustomColorPickerComponent
});

properties: {
  size: {
    label: 'Size',
    defaultValue: 'medium',
    options: ['small', 'medium', 'large'],
    condition: 'this.showSizeOption == true'
  }
}

Why: Custom property editors let you create easy-to-use input for end-users, such as sliders or styled dropdowns. The advanced options help you control logic, such as setting defaults, showing/hiding fields, etc., based on how users configure the tool.  

📄 Docs: Built-in Property Editors, Advanced Options

✅ Leverage custom blocks for reusable, brand-specific content

unlayer.registerBlock({
  name: 'brand-header',
  label: 'Brand Header',
  category: 'Brand Elements',
  design: {
    body: {
      rows: [
        {
          columns: [
            {
              contents: [
                {
                  type: 'text',
                  values: {
                    text: 'Welcome to Our Newsletter',
                    textAlign: 'center',
                    fontSize: '24px'
                  }
                }
              ]
            }
          ]
        }
      ]
    }
  }
});

Why: With custom blocks, you can create reusable content sections such as branded headers, footers, etc., so that your end-users just drag-and-drop the blocks instead of building them from scratch every time, keeping designs consistent and on-brand.

📄 Documentation: Custom Blocks 

✅ Enable dynamic content for personalization 

unlayer.init({
  mergeTags: {
    first_name: { name: 'First Name', value: '{{first_name}}' },
    user_type: { name: 'User Type', value: '{{user_type}}' }
  },
  designTags: {
    primary_color: '#FF6600',
    banner_text: 'Welcome Back!'
  }
});

{
  "displayCondition": {
    "attribute": "user_type",
    "operator": "equals",
    "value": "premium"
  }
}

Why: Enable personalization by defining merge tags, design tags, and display conditions using your app’s user data. This lets your end-users add dynamic values (names, segments, etc.),  change design elements, and show/hide content without any code. 

📄 Documentation: Merge Tags, Design Tags, Display Conditions, Dynamic Images

6. Troubleshooting Framework-Specific Integration Issues 

React, Angular, and Vue handle component rendering and lifecycles differently, and you might run into issues like repeated builder initialization, design loading errors, broken AMP blocks, or missing custom tools.

To help you avoid these issues before they occur (and fix them if they do), we’ve created framework-specific troubleshooting guides: 

📄 React: Troubleshooting Common Issues When Embedding Unlayer in React

📄 Angular: Angular Editor Development: Common Pitfalls & How to Fix Them

📄 Vue: 6 Common Vue Email Builder Issues and How to Avoid Them

7. Using the Developer Hub & CLI for Debugging and Control

✅ Use the Dev Hub to test and preview designs 

Developer Hub → Load Design → Export → Switch modes (email/page/popup)

Why: This lets you test design JSON, preview HTML/PDF/image/plain-text exports, and validate builder state across display modes without touching the live codebase.

✅ Use console.log to debug lifecycle events

unlayer.init({
  id: 'editor',
  onLoad: () => console.log('✔️ Editor loaded'),
  onDesignLoad: d => console.log('🌀 Design loaded:', d),
  onExport: data => console.log('📤 Export HTML snippet:', data.html.slice(0,100)),
});

Why: Capturing events and payloads in real time ensures that callbacks are behaving as expected and flags issues like failed integrations or missing design data very quickly. 

✅ Inspect the .exportHtml() payload directly

unlayer.exportHtml(data => {
  console.log('Full design JSON:', data.design);
  console.log('HTML output preview:', data.html.slice(0,200));
});

Why: Reviewing the raw export data helps you catch issues like broken layouts, missing merge tags, incorrect conditions, etc, before the design reaches the backend or rendering pipeline. 

✅ Automate design exports via CLI

# Example CLI workflow for batch exporting
unlayer-cli export \
  --design ./email_template.json \
  --output ./exports \
  --formats html,pdf,image

Why: This helps catch formatting issues, template bugs, or regressions early, and is especially helpful if you are managing large-scale or multi-tenant design systems.

8. Content Safety & Audit

✅ Enable content audit rules

unlayer.init({
  features: {
    contentAudit: {
      rules: ['requiredPreheader', 'mergeTagValidation']
    }
  }
});

Why: To automatically check required elements such as preheaders and ensure that all merge tags are appropriately filled, helping catch content issues before export. 

✅ Set preheader text & metadata

unlayer.init({
  design: {
    metadata: {
      preheader: 'Latest updates and offers inside',
      campaign_id: 'welcome_series'
    }
  }
});

Why: To improve how your emails appear in the inbox preview and to track and analyze campaign performance.

✅ Validate merge tags on export

unlayer.registerCallback('export', (data, done) => {
  if (data.html.includes('{{')) {
    alert('Merge tags are missing values.');
  }
  done();
});

Why: This catches personalization errors like {{first_name}} before they reach end-users,  preventing broken or unprofessional emails.  

9. Security & Compliance (If Applicable)

✅ Use SOC 2-compliant file storage

unlayer.init({
  fileUpload: {
    url: 'https://your-s3-upload-endpoint.com'
  }
});

Why: To ensure that uploaded files are securely handled using services such as Amazon S3 that are SOC 2 compliant and meet all the enterprise-grade security standards. 

✅ Secure file uploads with signed URLs

unlayer.registerCallback('image', (file, done) => {
  fetch('/api/get-signed-url')
    .then(res => res.json())
    .then(({ url }) => {
      fetch(url, {
        method: 'PUT',
        body: file.attachments[0]
      }).then(() => done({ progress: 100, url }));
    });
});

Why: This allows only authorized and time-limited access, reducing the risk of tampering or data misuse. 

✅ Sandbox custom JS tools using customJS

Custom JS

Pass custom JS to the builder using the customJS property during initialization.

unlayer.init({
  customJS: '//cdn.muicss.com/mui-0.9.28/js/mui.min.js',
});

Or, pass the JS source code like this: 

unlayer.init({
  customJS: [
    "
      console.log('I am custom JS!');
    "
  ]
});

Why: Custom tools loaded using customJS run inside the builder’s own environment. This keeps them isolated and prevents the script from interfering with your application and accessing sensitive resources. 

Note: For React applications, you have to bundle the custom tool code, host the file publicly, and then provide its URL in the customJS parameter.

10. Advanced Workflows (Optional)

✅ Use headless mode for server-side rendering

// Example: Render stored design JSON using your own renderer
fetch('/api/get-design').then(res => res.json()).then((design) => {
  unlayer.loadDesign(design);
});

Why: To store and manage your designs server-side and render only when needed—for full backend control.

✅ Auto-save user designs with onDesignChange

unlayer.init({
  projectId: 1234,
  onDesignChange: function (design) {
    fetch('/api/autosave', {
      method: 'POST',
      body: JSON.stringify(design),
      headers: { 'Content-Type': 'application/json' }
    });
  }
});

Why: To automatically save design changes as the user works, reducing data loss and improving UX during long editing sessions. 

✅ Integrate with your templating engine for API-driven content delivery

unlayer.exportHtml((data) => {
  const html = renderWithTemplateEngine(data.html);
  sendEmail(html);
});

Why: Export content from Unlayer and inject it into your server-side logic for dynamic emails/landing pages, or automated campaigns. 

So, What Really Matters?

Is it a builder that works or one that just belongs to your product?

The above-mentioned Unlayer best practices for developers go beyond integration. They are what help you deliver a builder that feels native and scalable.

Embed Unlayer!

Production-ready. Dev-friendly. Built for scale.

Sign Up →