Skip to content
notectl is live on DevHunt's tool of the week contest. Vote for notectl on DevHunt

Custom Fonts

notectl’s FontPlugin makes it easy to add custom fonts to your editor. It handles @font-face injection automatically — just provide font files and the plugin does the rest.

  1. Place your font files in your project’s public directory (e.g., public/fonts/)

  2. Define a FontDefinition with the font metadata and file sources:

    import type { FontDefinition } from '@notectl/core/plugins/font';
    const MY_FONT: FontDefinition = {
    name: 'Inter',
    family: "'Inter', sans-serif",
    category: 'sans-serif',
    fontFaces: [
    {
    src: "url('/fonts/Inter-Variable.woff2') format('woff2')",
    weight: '100 900',
    style: 'normal',
    },
    {
    src: "url('/fonts/Inter-Italic-Variable.woff2') format('woff2')",
    weight: '100 900',
    style: 'italic',
    },
    ],
    };
  3. Pass it to the FontPlugin:

    import { createEditor } from '@notectl/core';
    import { FontPlugin } from '@notectl/core/plugins/font';
    import { STARTER_FONTS } from '@notectl/core/fonts/starter';
    const editor = await createEditor({
    toolbar: [
    // ... other plugin groups
    [new FontPlugin({ fonts: [...STARTER_FONTS, MY_FONT] })],
    ],
    });

The plugin automatically injects @font-face CSS rules into the document head. No manual CSS needed.

interface FontDefinition {
/** Display name shown in the toolbar dropdown. */
name: string;
/** CSS font-family value, e.g. "'Fira Code', monospace". */
family: string;
/** Font category for grouping in the UI. */
category?: 'serif' | 'sans-serif' | 'monospace' | 'display' | 'handwriting';
/** @font-face descriptors. When provided, CSS rules are auto-injected. */
fontFaces?: FontFaceDescriptor[];
}
interface FontFaceDescriptor {
/** CSS src value, e.g. "url('/fonts/My.woff2') format('woff2')". */
src: string;
/** Font weight, e.g. '400' or '300 700' for variable fonts. */
weight?: string;
/** Font style, e.g. 'normal' or 'italic'. */
style?: string;
/** Font display strategy. Defaults to 'swap'. */
display?: string;
}

notectl ships with two embedded fonts that work without any font files:

import { STARTER_FONTS, FIRA_CODE, FIRA_SANS } from '@notectl/core/fonts/starter';
// Use both starter fonts
new FontPlugin({ fonts: [...STARTER_FONTS] })
// Or pick individually
new FontPlugin({ fonts: [FIRA_SANS, FIRA_CODE] })
Font Family Category Description
Fira Sans 'Fira Sans', sans-serif sans-serif Clean sans-serif for body text
Fira Code 'Fira Code', monospace monospace Monospace with programming ligatures

System fonts don’t need fontFaces — just specify the family:

const SYSTEM_FONTS: FontDefinition[] = [
{ name: 'Arial', family: 'Arial, sans-serif', category: 'sans-serif' },
{ name: 'Georgia', family: 'Georgia, serif', category: 'serif' },
{ name: 'Courier New', family: "'Courier New', monospace", category: 'monospace' },
];
new FontPlugin({ fonts: SYSTEM_FONTS });

Variable fonts use weight ranges in their descriptor:

const INTER: FontDefinition = {
name: 'Inter',
family: "'Inter', sans-serif",
category: 'sans-serif',
fontFaces: [
{
src: "url('/fonts/Inter-Variable.woff2') format('woff2')",
weight: '100 900', // Variable weight range
style: 'normal',
},
{
src: "url('/fonts/Inter-Italic-Variable.woff2') format('woff2')",
weight: '100 900',
style: 'italic',
},
],
};

For maximum browser compatibility, provide multiple formats:

const MY_FONT: FontDefinition = {
name: 'My Font',
family: "'My Font', sans-serif",
fontFaces: [
{
src: [
"url('/fonts/MyFont.woff2') format('woff2')",
"url('/fonts/MyFont.woff') format('woff')",
"url('/fonts/MyFont.ttf') format('truetype')",
].join(', '),
weight: '400',
style: 'normal',
},
{
src: "url('/fonts/MyFont-Bold.woff2') format('woff2')",
weight: '700',
style: 'normal',
},
],
};
interface FontConfig {
/** Fonts available in the editor. */
fonts: FontDefinition[];
/** Name of the default font. Selecting it removes the font mark. */
defaultFont?: string;
/** Custom locale for toolbar labels. */
locale?: FontLocale;
}

The defaultFont option specifies which font is the editor’s base font. When a user selects this font, the font mark is removed rather than applied (since the text already renders in that font):

new FontPlugin({
fonts: [INTER, ...STARTER_FONTS],
defaultFont: 'Inter', // Must match a FontDefinition.name
});

If not specified, the first font in the list is treated as default.

Font and font size work independently but pair well in the toolbar:

const editor = await createEditor({
toolbar: [
[new TextFormattingPlugin()],
[
new FontPlugin({ fonts: [...STARTER_FONTS, INTER] }),
new FontSizePlugin({ sizes: [12, 14, 16, 20, 24, 32, 48], defaultSize: 16 }),
],
],
});

When a font has fontFaces, the plugin injects a <style> tag into the document head:

@font-face {
font-family: 'Inter';
src: url('/fonts/Inter-Variable.woff2') format('woff2');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}

The style element is automatically cleaned up when the editor is destroyed.

You can use Google Fonts by loading the stylesheet separately and defining a system-style font:

<!-- In your HTML head -->
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap" rel="stylesheet">
const ROBOTO: FontDefinition = {
name: 'Roboto',
family: "'Roboto', sans-serif",
category: 'sans-serif',
// No fontFaces needed — Google Fonts handles @font-face
};