---
url: >-
  https://plugin.stripo.email/extensions/tutorials/examples/integrations/external-custom-font.md
---
# Integrate an External Custom Font

## Overview

The External Custom Font integration allows users to add and manage custom web fonts directly within the Stripo Email Editor. This integration provides a seamless experience for incorporating brand-specific typography, web fonts from CDNs, or self-hosted font files into email templates while maintaining full control over your font library.

### What You'll Build

In this tutorial, you'll create a fully functional custom font selector that:

* Extends the default font family dropdown with an "Add Custom Font" option
* Opens a modal dialog to collect font information (name, CSS font-family, URL)
* Validates all required font properties before submission
* Integrates seamlessly with the editor's font management system
* Provides visual feedback with modern, user-friendly interface

::: image-wrap
![](/img/plugin/new/image216.webp){width=1999 height=795}
:::

::: image-wrap
![](/img/plugin/new/image217.webp){width=580}
:::

::: image-wrap
![](/img/plugin/new/image218.webp){width=1999 height=788}
:::

### Use Cases

* **Brand Typography**: Incorporate your organization's proprietary fonts into email templates
* **Google Fonts Integration**: Add fonts from Google Fonts or other web font CDNs
* **Self-Hosted Fonts**: Connect to fonts hosted on your own servers or CDN
* **Font Marketplace Integration**: Link to third-party font providers like Adobe Fonts or Typekit
* **Multi-Brand Support**: Enable different font sets for different brands or clients

## Prerequisites

Before starting this tutorial, ensure you have:

* Node.js version 22.x or higher installed
* Basic understanding of JavaScript ES6+ syntax
* Familiarity with the Stripo Extensions SDK
* Understanding of CSS `@font-face` and web fonts

## Understanding the Interface

The External Custom Font integration requires two key components:

### 1. Custom UI Element

Create a custom UI element that extends `UIElement` to wrap the default font selector and add custom functionality:

```typescript
class CustomFontFamilySelectUIElement extends UIElement {
  getId(): string;                          // Unique element ID
  getTemplate(): string;                    // HTML template wrapping original selector
  onRender(container: HTMLElement): void;   // Setup event listeners
  onDestroy(): void;                        // Cleanup event listeners
  onAttributeUpdated(name: string, value: any): void;  // Handle attribute changes
  getValue(): string;                       // Get current font value
  setValue(value: string): void;            // Set font value programmatically
}
```

### 2. UI Element Tag Registry

Register your custom UI element to replace the default font family selector:

```typescript
class ExtensionTagRegistry extends UIElementTagRegistry {
  registerUiElements(uiElementsTagsMap: Record<string, string>): void;
}
```

### Custom Font Object Structure

When adding a custom font, your implementation must provide an object with the following structure:

```typescript
{
  name: string;        // Display name (e.g., "Montserrat Bold")
  fontFamily: string;  // CSS font-family value (e.g., "'Montserrat', sans-serif")
  url: string;         // Font resource URL (e.g., "https://fonts.googleapis.com/...")
}
```

::: tip Font URL Formats
Font URLs can point to various resources:

* **Google Fonts**: `https://fonts.googleapis.com/css2?family=Roboto:wght@400;700`
* **Self-hosted**: `https://cdn.yoursite.com/fonts/custom-font.woff2`
* **Adobe Fonts**: `https://use.typekit.net/abc1234.css`
* Any valid web font resource that uses `@font-face` CSS rules
  :::

## Step 1: Project Setup

Create your project structure and install dependencies according to the [Getting Started](/extensions/getting-started) guide.

Your project directory structure should look like this:

```bash
external-custom-font/
├── index.html
├── src/
│   ├── creds.js
│   ├── index.js
│   ├── extension.js
├── package.json
└── vite.config.js
```

## Step 2: Create the Custom Font Selector UI Element

Create a new file `src/CustomFontFamilySelectUIElement.js` with the following implementation:

```javascript
import {ADD_CUSTOM_FONT_OPTION, UEAttr, UIElement} from '@stripoinc/ui-editor-extensions';

export const CUSTOM_FONT_FAMILY_SELECT_UI_ELEMENT_ID = 'custom-font-family-select';
export const ORIGINAL_FONT_FAMILY_SELECT_ID = 'original-font-family-select';

export class CustomFontFamilySelectUIElement extends UIElement {
    getId() {
        return CUSTOM_FONT_FAMILY_SELECT_UI_ELEMENT_ID;
    }

    getTemplate() {
        return `<${ORIGINAL_FONT_FAMILY_SELECT_ID}
            id="originalSelect"
            style="width: 100%;"
            ${UEAttr.FONT_FAMILY_SELECT.addCustomFontOption}="+ Insert custom font">
        </${ORIGINAL_FONT_FAMILY_SELECT_ID}>`;
    }

    onRender(container) {
        this.listener = this._onChange.bind(this);
        this.originalSelect = container.querySelector('#originalSelect');
        this.originalSelect.addEventListener('change', this.listener);
    }

    onDestroy() {
        this.originalSelect.removeEventListener('change', this.listener);
    }

    onAttributeUpdated(name, value) {
        this.originalSelect.setUIEAttribute(name, value);
        super.onAttributeUpdated(name, value);
    }

    getValue() {
        return this.originalSelect.value;
    }

    setValue(value) {
        this.originalSelect.value = value;
    }

    _onChange(event) {
        if (event.target.value !== ADD_CUSTOM_FONT_OPTION) {
            this.api.triggerValueChange(event.target.value);
        } else if (!this.dialog) {
            this._showDialog();
        }
    }
}
```

### Key Components Explained

* **Template Structure**:
  * Wraps the original font selector using `ORIGINAL_FONT_FAMILY_SELECT_ID`
  * Adds custom option via `UEAttr.FONT_FAMILY_SELECT.addCustomFontOption` attribute
  * The custom option text is "+ Insert custom font"

* **Event Handling**:
  * `onRender()`: Attaches change listener to the select element
  * `_onChange()`: Distinguishes between regular font selection and custom font trigger
  * When `ADD_CUSTOM_FONT_OPTION` is selected, opens the custom font dialog

* **Value Management**:
  * `getValue()` and `setValue()`: Proxy methods that delegate to the original select
  * `onAttributeUpdated()`: Forwards attribute changes to the wrapped element

::: warning Important Constants
The SDK provides `ADD_CUSTOM_FONT_OPTION` constant to identify the special custom font option. Never hardcode this value, always import it from the SDK.
:::

## Step 3: Create the Custom Font Dialog

Add the dialog creation and management methods to `CustomFontFamilySelectUIElement.js`:

### Dialog Display Method

```javascript
_showDialog() {
    this.dialog = document.createElement('div');
    this.dialog.innerHTML = this._getDialogTemplate();
    this.api.ignoreClickOutside(true);
    document.body.appendChild(this.dialog);

    // Add event listeners
    this.dialog.querySelector('#confirm').addEventListener('click', () => this._submitDialog());
    this.dialog.querySelector('#cancel').addEventListener('click', () => this._closeDialog());

    // Add input event listeners to hide error message when user starts typing
    const inputs = this.dialog.querySelectorAll('input');
    inputs.forEach(input => {
        input.addEventListener('input', () => {
            const errorMsg = this.dialog.querySelector('#error-message');
            if (errorMsg) {
                errorMsg.style.display = 'none';
            }
        });
    });
}
```

### Dialog Template Method

```javascript
_getDialogTemplate() {
    return `<div style="
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background: rgba(0, 0, 0, 0.5);
        display: flex;
        align-items: center;
        justify-content: center;
        z-index: 9999;">
        <div style="
            background: #ffffff;
            border-radius: 8px;
            box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15);
            width: 420px;
            max-width: 90vw;
            animation: fadeIn 0.2s ease-out;">
            <div style="
                padding: 24px;
                border-bottom: 1px solid #e5e7eb;">
                <h2 style="
                    margin: 0;
                    font-size: 20px;
                    font-weight: 600;
                    color: #1f2937;
                    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;">
                    Add Custom Font
                </h2>
                <p style="
                    margin: 8px 0 0 0;
                    font-size: 14px;
                    color: #6b7280;
                    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;">
                    Configure your custom font settings
                </p>
            </div>
            <div style="padding: 24px;">
                <div style="margin-bottom: 20px;">
                    <label style="
                        display: block;
                        margin-bottom: 8px;
                        font-size: 14px;
                        font-weight: 500;
                        color: #374151;
                        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;">
                        Font Name
                    </label>
                    <input id="name"
                        placeholder="e.g., My Custom Font"
                        style="
                            width: 100%;
                            padding: 10px 12px;
                            border: 1px solid #d1d5db;
                            border-radius: 6px;
                            font-size: 14px;
                            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
                            transition: all 0.2s;
                            box-sizing: border-box;
                            outline: none;"
                        onfocus="this.style.borderColor='#3b82f6'; this.style.boxShadow='0 0 0 3px rgba(59, 130, 246, 0.1)'"
                        onblur="this.style.borderColor='#d1d5db'; this.style.boxShadow='none'">
                </div>
                <div style="margin-bottom: 20px;">
                    <label style="
                        display: block;
                        margin-bottom: 8px;
                        font-size: 14px;
                        font-weight: 500;
                        color: #374151;
                        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;">
                        CSS Font Family
                    </label>
                    <input id="fontFamily"
                        placeholder="e.g., 'My Font', sans-serif"
                        style="
                            width: 100%;
                            padding: 10px 12px;
                            border: 1px solid #d1d5db;
                            border-radius: 6px;
                            font-size: 14px;
                            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
                            transition: all 0.2s;
                            box-sizing: border-box;
                            outline: none;"
                        onfocus="this.style.borderColor='#3b82f6'; this.style.boxShadow='0 0 0 3px rgba(59, 130, 246, 0.1)'"
                        onblur="this.style.borderColor='#d1d5db'; this.style.boxShadow='none'">
                </div>
                <div style="margin-bottom: 24px;">
                    <label style="
                        display: block;
                        margin-bottom: 8px;
                        font-size: 14px;
                        font-weight: 500;
                        color: #374151;
                        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;">
                        Font URL
                    </label>
                    <input id="url"
                        placeholder="e.g., https://fonts.googleapis.com/..."
                        style="
                            width: 100%;
                            padding: 10px 12px;
                            border: 1px solid #d1d5db;
                            border-radius: 6px;
                            font-size: 14px;
                            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
                            transition: all 0.2s;
                            box-sizing: border-box;
                            outline: none;"
                        onfocus="this.style.borderColor='#3b82f6'; this.style.boxShadow='0 0 0 3px rgba(59, 130, 246, 0.1)'"
                        onblur="this.style.borderColor='#d1d5db'; this.style.boxShadow='none'">
                </div>
                <div id="error-message" style="
                    display: none;
                    padding: 12px;
                    background: #fee2e2;
                    border: 1px solid #fecaca;
                    border-radius: 6px;
                    color: #991b1b;
                    font-size: 14px;
                    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
                    margin-bottom: 20px;">
                    All fields are required. Please fill in all the information.
                </div>
                <div style="
                    display: flex;
                    gap: 12px;
                    justify-content: flex-end;">
                    <button
                        id="cancel"
                        style="
                            padding: 10px 20px;
                            border: 1px solid #d1d5db;
                            border-radius: 6px;
                            background: #ffffff;
                            color: #374151;
                            font-size: 14px;
                            font-weight: 500;
                            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
                            cursor: pointer;
                            transition: all 0.2s;">
                        Cancel
                    </button>
                    <button id="confirm"
                        style="
                            padding: 10px 20px;
                            border: none;
                            border-radius: 6px;
                            background: #34c759;
                            color: #ffffff;
                            font-size: 14px;
                            font-weight: 500;
                            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
                            cursor: pointer;
                            transition: all 0.2s;">
                        Add Font
                    </button>
                </div>
            </div>
            <div style="
                padding: 16px 24px;
                border-top: 1px solid #e5e7eb;
                background-color: #fef3c7;
                border-radius: 0 0 8px 8px;
                text-align: center;">
                <p style="
                    margin: 0;
                    font-size: 13px;
                    color: #92400e;
                    font-weight: 500;
                    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;">
                    <span style="font-weight: 700; color: #d97706;">⚠️ Notice:</span> This popup window is not part of the plugin. It is intended solely for demonstration purposes and can be implemented independently in any desired way.
                </p>
            </div>
        </div>
    </div>
    <style>
        @keyframes fadeIn {
            from {
                opacity: 0;
                transform: scale(0.95);
            }
            to {
                opacity: 1;
                transform: scale(1);
            }
        }
    </style>`;
}
```

### Dialog Features Explained

1. **Modal Overlay**: Fixed position overlay with semi-transparent background and blur effect
2. **Form Fields**:
   * **Font Name**: Display name shown in the font selector dropdown
   * **CSS Font Family**: The actual CSS `font-family` value (e.g., "'Roboto', sans-serif")
   * **Font URL**: Link to the font resource or stylesheet
3. **Input Enhancement**: Focus states with blue border and shadow for better UX
4. **Error Display**: Hidden error message that shows when validation fails
5. **Animations**: Smooth fade-in animation using CSS keyframes
6. **Responsive Design**: Uses `max-width: 90vw` to ensure dialog fits on small screens

::: tip Click Outside Prevention
The `this.api.ignoreClickOutside(true)` call prevents the editor from closing the dialog when clicking inside it. Remember to call `ignoreClickOutside(false)` when closing the dialog.
:::

## Step 4: Implement Dialog Submission and Validation

Add the submission and validation logic to handle font addition:

### Submit Dialog Method

```javascript
_submitDialog() {
    const nameInput = this.dialog.querySelector('#name');
    const fontFamilyInput = this.dialog.querySelector('#fontFamily');
    const urlInput = this.dialog.querySelector('#url');

    // Reset any previous error states
    [nameInput, fontFamilyInput, urlInput].forEach(input => {
        input.style.borderColor = '#d1d5db';
    });

    // Validate inputs
    let hasError = false;
    if (!nameInput.value.trim()) {
        nameInput.style.borderColor = '#ef4444';
        hasError = true;
    }
    if (!fontFamilyInput.value.trim()) {
        fontFamilyInput.style.borderColor = '#ef4444';
        hasError = true;
    }
    if (!urlInput.value.trim()) {
        urlInput.style.borderColor = '#ef4444';
        hasError = true;
    }

    if (hasError) {
        // Show error message
        const errorMsg = this.dialog.querySelector('#error-message');
        if (errorMsg) {
            errorMsg.style.display = 'block';
        }
        return;
    }

    const newFont = {
        name: nameInput.value.trim(),
        fontFamily: fontFamilyInput.value.trim(),
        url: urlInput.value.trim(),
    }
    this.api.addCustomFont(newFont);
    this._closeDialog();
}
```

### Close Dialog Method

```javascript
_closeDialog() {
    if (this.dialog) {
        this.dialog.remove();
        this.dialog = undefined;
        this.api.ignoreClickOutside(false);
        this.originalSelect.value = '';
    }
}
```

### Validation Logic Explained

1. **Input Retrieval**: Gets references to all three input fields
2. **State Reset**: Clears any previous error border colors
3. **Validation**:
   * Checks each field is not empty using `trim()`
   * Highlights invalid fields with red border (`#ef4444`)
   * Sets `hasError` flag when validation fails
4. **Error Display**: Shows error message if any field is invalid
5. **Font Registration**: If validation passes, calls `this.api.addCustomFont()` with font object
6. **Cleanup**: Closes dialog and resets the select value

::: tip Font Family Format
The `fontFamily` field should follow CSS `font-family` syntax:

* Single font: `'Roboto'`
* With fallback: `'Roboto', sans-serif`
* Multiple words: `'Open Sans', Arial, sans-serif`

Always include quotes around font names with spaces.
:::

## Step 5: Create the UI Element Tag Registry

Create `src/ExtensionTagRegistry.js` to register your custom font selector as a replacement for the default:

```javascript
import {UIElementTagRegistry, UIElementType} from '@stripoinc/ui-editor-extensions';
import {CUSTOM_FONT_FAMILY_SELECT_UI_ELEMENT_ID, ORIGINAL_FONT_FAMILY_SELECT_ID} from './CustomFontFamilySelectUIElement';

export class ExtensionTagRegistry extends UIElementTagRegistry {
    registerUiElements(uiElementsTagsMap) {
        uiElementsTagsMap[ORIGINAL_FONT_FAMILY_SELECT_ID] = uiElementsTagsMap[UIElementType.FONT_FAMILY_SELECT];
        uiElementsTagsMap[UIElementType.FONT_FAMILY_SELECT] = CUSTOM_FONT_FAMILY_SELECT_UI_ELEMENT_ID;
    }
}
```

### Tag Registry Logic Explained

The registry performs two critical mappings:

1. **Preserve Original Selector**:
   ```javascript
   uiElementsTagsMap[ORIGINAL_FONT_FAMILY_SELECT_ID] =
       uiElementsTagsMap[UIElementType.FONT_FAMILY_SELECT];
   ```
   * Saves the original font selector implementation
   * Makes it available under `ORIGINAL_FONT_FAMILY_SELECT_ID`
   * Allows your custom element to wrap and reuse the original

2. **Replace Default Selector**:
   ```javascript
   uiElementsTagsMap[UIElementType.FONT_FAMILY_SELECT] =
       CUSTOM_FONT_FAMILY_SELECT_UI_ELEMENT_ID;
   ```
   * Maps the standard font selector type to your custom implementation
   * Ensures all font selector instances use your enhanced version
   * Seamlessly integrates with the editor without modifying existing code

::: warning Order Matters
The order of these two statements is critical. First save the original selector reference, then replace it with your custom implementation. Reversing the order will break the functionality.
:::

## Step 6: Register the Extension

Create `src/extension.js` to register your custom font integration with the Stripo extension system:

```javascript
import {ExtensionBuilder} from '@stripoinc/ui-editor-extensions';
import {CustomFontFamilySelectUIElement} from './CustomFontFamilySelectUIElement';
import {ExtensionTagRegistry} from './ExtensionTagRegistry';

const extension = new ExtensionBuilder()
    .addUiElement(CustomFontFamilySelectUIElement)
    .withUiElementTagRegistry(ExtensionTagRegistry)
    .build();

export default extension;
```

### Extension Registration Explained

The `ExtensionBuilder` provides a fluent API for composing extensions:

1. **`.addUiElement(CustomFontFamilySelectUIElement)`**:
   * Registers your custom UI element class with the extension system
   * Makes the element available for use in the editor
   * The editor will instantiate this class when needed

2. **`.withUiElementTagRegistry(ExtensionTagRegistry)`**:
   * Registers your tag registry to control UI element mapping
   * Tells the editor to replace the default font selector with your custom one
   * Enables the wrapper pattern by preserving access to the original selector

3. **`.build()`**:
   * Constructs and returns the final extension configuration object
   * This object is passed to the editor during initialization

::: tip Multiple Integrations
You can chain multiple integration methods within a single extension:

```javascript
new ExtensionBuilder()
    .addUiElement(CustomFontFamilySelectUIElement)
    .withUiElementTagRegistry(ExtensionTagRegistry)
    .withExternalImageLibrary(MyExternalImageLibrary)
    .withExternalVideosLibrary(MyExternalVideosLibrary)
    .build();
```

This allows combining custom fonts with other external integrations.
:::

## Step 7: Run the Development Server

Your implementation is now ready for testing.

### Start the Development Server

```bash
npm run dev
```

This command will:

1. Start the Vite development server on `http://localhost:3000`
2. Automatically open your default browser
3. Load the Stripo Editor with your custom font integration

## Complete Example

For a full working example, check out the complete implementation in our [GitHub repository](https://github.com/stripoinc/stripo-plugin-samples/tree/main/extensions/javascript/external-custom-font).
