---
title: Equation
description: The equation plugin adds Notion's block and inline equations, TeX drawn by KaTeX loaded from a CDN on demand, edited in a field under the equation with a live preview.
icon: sigma
---

`createEquationPlugin` adds Notion's equations: a block equation (`equation`) and an inline equation in text (`inlineEquation`), each holding its TeX source and drawn by [KaTeX](https://katex.org). It is not a default plugin: list it.

```svelte title="Editor.svelte" check
<script lang="ts">
	import { Edytor, createEquationPlugin } from 'edytor';

	const equations = createEquationPlugin();
</script>

<Edytor plugins={[equations]} />
```

Your app installs, bundles and imports nothing for KaTeX. The first time an equation is drawn in a browser, the plugin loads KaTeX from [jsDelivr](https://www.jsdelivr.com): its module (`katex.mjs`, about 75 KB compressed) and its stylesheet (`katex.min.css`, added once to the page's head, which loads KaTeX's fonts as a formula needs them). Pages without math download none of it. The version is pinned: `KATEX_VERSION` (the KaTeX `edytor` is tested with), at `KATEX_CDN`, both exported. Every view and every equation plugin on a page share one load.

Until KaTeX loads, in a server render, and when it fails to load (offline, a blocked CDN), an equation shows its TeX source; nothing throws, and the console says why.

## Where KaTeX comes from

The `katex` option picks the source:

| `katex` | KaTeX comes from |
| --- | --- |
| absent | jsDelivr: `KATEX_CDN`, the default |
| a URL | another copy of KaTeX's `dist` folder (your CDN, your own static files): it must hold `katex.mjs`, `katex.min.css` and the `fonts/` the stylesheet names |
| `() => import('katex')` | your bundle: install `katex` (an optional peer dependency, `>=0.16`) and import its stylesheet, `katex/dist/katex.min.css`, yourself |
| `false` | nowhere: equations show their TeX source |

```ts title="katex-sources.ts" check
import { KATEX_VERSION, createEquationPlugin } from 'edytor';

// Your own copy of KaTeX's `dist` folder, the same version.
export const selfHosted = createEquationPlugin({
	katex: `https://static.example.com/katex@${KATEX_VERSION}/dist/`
});

// No equation drawing at all: the TeX source shows.
export const sourceOnly = createEquationPlugin({ katex: false });
```

A bundled KaTeX loads as a chunk of your app, the first time an equation shows:

```svelte title="Bundled.svelte" check
<script lang="ts">
	import 'katex/dist/katex.min.css';
	import { Edytor, createEquationPlugin } from 'edytor';

	const equations = createEquationPlugin({ katex: () => import('katex') });
</script>

<Edytor plugins={[equations]} />
```

### Content Security Policy

With the default, a page with a Content Security Policy allows jsDelivr for the module, the stylesheet and the fonts:

```text
script-src 'self' https://cdn.jsdelivr.net;
style-src 'self' https://cdn.jsdelivr.net;
font-src 'self' https://cdn.jsdelivr.net;
```

With a URL, allow its host instead. A policy that allows no other host (or that requires subresource integrity, which a module import cannot carry) bundles KaTeX with `katex: () => import('katex')`.

## Creating equations

- **Block equation**: the slash menu's "Block equation" (group `Advanced blocks`, keywords `math`, `tex`, `latex`, `katex`, `formula`, `equation`). Like a divider it replaces an empty line and goes after a line that holds text; its editor opens at once.
- **Inline equation**: the slash menu's "Inline equation" (group `Inline`), or <kbd>Mod</kbd> + <kbd>Shift</kbd> + <kbd>E</kbd>. Over selected text in one block, the text becomes the equation's source, in one undo step: the text's removal leads the `insertInline` operation in one plan, so a hook that refuses either keeps the text; at a caret, an empty equation is inserted and its editor opens.
- **Typed**: `$$E=mc^2$$` in text becomes an inline equation when you type the last `$`, the caret after it. One undo step gives the typed text back. Input rules do not run in code blocks.

## Editing

A click on an equation in an editable view, or <kbd>Enter</kbd> on a selected one, opens its editor under it: the TeX source in a field, and Done. Every keystroke writes the source (`data.expression`), so the equation itself is the live preview, and the typing is one undo step. TeX KaTeX cannot read shows the source in red, titled "Invalid equation" with KaTeX's message, which the editor also shows under the field.

| Key | In the editor |
| --- | --- |
| <kbd>Enter</kbd> | Done: closes the editor |
| <kbd>Shift</kbd> + <kbd>Enter</kbd> | A new line in a block equation; Done in an inline one |
| <kbd>Escape</kbd> | Closes the editor, the source as typed |

A press or a focus outside the editor closes it too. After it closes, a block equation stays selected and the caret goes after an inline one. An inline equation closed empty is removed, as in Notion. A readonly view draws equations and opens no editor.

## Options

`createEquationPlugin(options)` takes an `EquationPluginOptions`:

| Prop | Type | Default | Description |
| - | - | - | - |
| `katex?` | `KatexLoader \| string \| false` | - | Where KaTeX comes from, the first time an equation shows in a browser: absent, jsDelivr (`KATEX_CDN`); a URL, another copy of KaTeX's `dist` folder; a loader, your bundle (`() => import('katex')`, its stylesheet imported by your app); `false`, none (the TeX source shows). See [Where KaTeX comes from](#where-katex-comes-from). |
| `macros?` | `Record<string, string>` | - | TeX macros every equation reads: `{ '\\RR': '\\mathbb{R}' }`. |
| `panel?` | `Snippet<[EquationEditor]>` | - | The editor's panel, under the edited equation (without it, nothing opens); `{@attach editor.field}` on your source field keeps its focus and keys. See [Menus and handles](/docs/customization/menus#equation-editor). |
| `placeholder?` | `Snippet<[EquationPlaceholderPayload]>` | - | An empty equation's markup, replacing the default (`√x` and the view's placeholder): `{ display, label }`, `display` true for a block equation, `label` the view's `equation.blockPlaceholder` or `inlinePlaceholder`. A click on it opens the editor, as on any equation. |
| `error?` | `Snippet<[EquationErrorPayload]>` | - | The markup of an equation KaTeX cannot read, replacing the default (its source, KaTeX's message in its title): `{ display, expression, error, label }`, `error` KaTeX's message, `label` the view's `equation.invalid`. |

KaTeX runs untrusted (`trust: false`): `\href`, `\url`, `\includegraphics` and the HTML extensions draw nothing that links or loads, so its markup is safe to show for any peer's TeX. `strict` warnings are off.

```ts title="equations.ts" check
import { createEquationPlugin } from 'edytor';

export const equations = createEquationPlugin({
	macros: { '\\RR': '\\mathbb{R}' }
});
```

The placeholder and the error are drawn inside the equation, which keeps its click, its selection and its void element: your snippets draw only what shows.

```svelte title="Equations.svelte" check
<script lang="ts">
	import {
		Edytor,
		createEquationPlugin,
		type EquationErrorPayload,
		type EquationPlaceholderPayload
	} from 'edytor';

	const equations = createEquationPlugin({ placeholder, error });
</script>

{#snippet placeholder(empty: EquationPlaceholderPayload)}
	<span class="math-empty">∑ {empty.label}</span>
{/snippet}

{#snippet error(failed: EquationErrorPayload)}
	<code class="math-error" title={failed.error}>{failed.expression}</code>
{/snippet}

<Edytor plugins={[equations]} />
```

## The kinds

| Kind | Where | Data | Created by |
| --- | --- | --- | --- |
| `equation` | a block (`div`) | `{ expression: string }` | "Block equation" (`block.equation`, √x, `Advanced blocks`) |
| `inlineEquation` | an inline atom in text | `{ expression: string }` | "Inline equation" (`equation.inline`), <kbd>Mod</kbd> + <kbd>Shift</kbd> + <kbd>E</kbd>, `$$…$$` |

The block equation is void and shows no text of its own, like a divider; its role is in `defaultSemantics` (`equationKinds`), so the room and headless documents read it alike. The inline equation is an atom: the caret steps over it and a selection takes it whole.

```ts title="seed.ts" check
import type { JSONDoc } from 'edytor';

export const value: JSONDoc = {
	children: [
		{
			type: 'paragraph',
			content: [
				{ text: 'Euler: ' },
				{ type: 'inlineEquation', data: { expression: 'e^{i\\pi} + 1 = 0' } }
			]
		},
		{ type: 'equation', data: { expression: '\\int_0^1 x^2\\,dx = \\frac{1}{3}' } }
	]
};
```

## Clipboard

Copying writes each equation as MathML whose `application/x-tex` annotation is its source, inside KaTeX's `katex` class (a block equation's `<math display="block">` in a `div[data-edytor-equation]`), so other apps paste math, and the plain text is the source. HTML import reads equations back from KaTeX's markup (`.katex`, `.katex-display`) or a bare `<math>` with a TeX annotation (or an `alttext`): a `display="block"` one as a block equation, any other as an inline equation in its line. Without the plugin, math imports nothing.

## Styling

| Selector | Matches |
| --- | --- |
| `[data-edytor-equation]` | A block equation's drawing (`[data-empty]` when it has no source, `[data-editing]` while edited) |
| `[data-edytor-inline-equation]` | An inline equation (`[data-empty]`, `[data-editing]`, `[data-selected]`) |
| `[data-edytor-equation-placeholder]` | An empty equation's placeholder (the default markup) |
| `[data-edytor-equation-error]` | TeX KaTeX cannot read, shown as its source (the default markup) |
| `[data-edytor-equation-source]` | The source shown while KaTeX loads, or without it |
| `[data-edytor-equation-editor]` | The editor, in the overlay (`data-display="block"` or `"inline"`); `--edytor-equation-editor-background` |

The plugin's own stylesheet only lets a wide block equation scroll sideways; the rest (the hover and editing shade, the centred drawing, the placeholder's panel, the source's monospace font, the error's red) is the [Notion theme](/docs/customization/styling#notion-theme)'s. KaTeX's own classes (`.katex`, `.katex-display`) style the drawing, from KaTeX's stylesheet: the plugin adds it to the page's head with the CDN default or a URL (`link[data-edytor-katex]`); with your own loader, your app imports it.
