Skip to content

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';
    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, FontPlugin, STARTER_FONTS } from '@notectl/core';
    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';
// Use both starter fonts
new FontPlugin({ fonts: [...STARTER_FONTS] })
// Or pick individually
new FontPlugin({ fonts: [FIRA_SANS, FIRA_CODE] })
FontFamilyCategoryDescription
Fira Sans'Fira Sans', sans-serifsans-serifClean sans-serif for body text
Fira Code'Fira Code', monospacemonospaceMonospace 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;
/** Render a separator after the font toolbar item. */
separatorAfter?: boolean;
}

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
};