MathInput

Getting started

MathInput is a web component. It works in any framework, or none.

Install

npm install @mathinput/element
# React apps also:
npm install @mathinput/react

Use it in plain HTML or any framework

<script type="module">
  import "@mathinput/element/define";        // registers <math-input>
  import "@mathinput/element/mathinput.css"; // default styles
</script>

<math-input subject="maths" label="Expression" submit-on-enter></math-input>

<script type="module">
  const input = document.querySelector("math-input");
  input.addEventListener("submit", (e) => {
    const value = e.detail;           // see Output formats
    console.log(value.latex);         // x=\frac{1}{2}
    console.log(value.text);          // x = 1/2
  });
</script>

Give every field an accessible name with label, aria-label or aria-labelledby.

Use it in React

import { MathInput } from "@mathinput/react";
import "@mathinput/element/mathinput.css";

export function EquationField() {
  return (
    <MathInput
      subject="chemistry"
      label="Balanced equation"
      submitOnEnter
      onSubmit={(v) => save(v.doc)}
    />
  );
}

Pass value and onInput for a controlled field; it never resets the caret while the user types.

Show a stored expression

<math-input readonly latex="\frac{-b\pm\sqrt{b^{2}-4ac}}{2a}"></math-input>

Read-only fields have no caret or keypad and are read out by screen readers.

One expression at a time

Each field holds one expression. For multi-line work, such as the steps of a calculation, keep the list in your app: on submit, store e.detail, show it read-only, and call input.clear().

Fonts

The expression uses STIX Two Text if it is available, then Cambria Math and Times. Load STIX Two from your own assets (for example the @fontsource/stix-two-text package) or set another font with the math-font attribute or --mi-font-math.

API reference

Attributes

Attribute Values Default Meaning
subject maths, chemistry, physics maths Typing rules, keypad and output style.
latex LaTeX Initial content. Unsupported LaTeX fires parse-error.
label text Accessible name (or use aria-label / aria-labelledby).
placeholder text Enter an expression Shown when empty.
keypad auto, always, never, collapsed auto auto: open on touch devices, behind a toggle with a mouse.
keypad-container element id Render the keypad inside another element.
submit-on-enter boolean off Enter and the ↵ key fire submit.
readonly boolean off Display only.
disabled boolean off
autoreplace false to turn off on Keyboard shortcuts such as typing sqrt or pi.
theme auto, light, dark auto Built-in colour scheme.
math-font font family STIX Two Text Font for the expression.

Properties

Property Type
value MathDocument The expression tree. Setting it replaces the content and clears undo.
latex string Current LaTeX; setting it parses LaTeX.
subject, readOnly, disabled Mirror the attributes.
keypadLayout KeypadLayout | KeypadPatch Replace or trim the keypad; see Keypad configuration.
keypadOpen boolean Open or close the keypad.
editor Editor The headless editor, for advanced use.

Methods

focus(), blur(), clear(), undo(), redo(), selectAll(), commit() (fires change), getValue(), setValue(doc), insert(nodeOrNodes), execute(command) where command is one of moveLeft, moveRight, moveUp, moveDown, moveHome, moveEnd, moveToNextPlaceholder, moveToPreviousPlaceholder, exitTemplate, deleteBackward, deleteForward, selectAll, clear, undo, redo.

Events

All events bubble and cross shadow roots.

Event detail When
input value Every edit.
change value On leaving the field after edits, or commit().
submit value Enter or ↵, with submit-on-enter. Not fired when empty.
parse-error { source, input, message } Pasted text or the latex attribute could not be read.
keypad-toggle { open } The keypad was opened or closed.

The value is described in Output formats.

Keyboard

Keys Action
/ Fraction (the number or term before the caret becomes the top)
^ _ Power, subscript
( ) [ ] Brackets, one side at a time: type ( before existing work and ) after it
| Modulus
<= >= != -> ~ ≤ ≥ ≠ → ≈
sqrt pi theta sin … Replaced as you type (turn off with autoreplace="false")
Arrows, Home, End Move; with Shift, select
Tab Next empty box, then leave the field
Ctrl/⌘ + Z, Y, A, C, X, V Undo, redo, select all, copy, cut, paste

In chemistry, digits after an element become subscripts (H2O), and a two-letter element is recognised as you type (Cl).

Packages

Package Contents
@mathinput/core Tree, headless editor, serialisers, parsers, keypad presets. No DOM; usable on a server.
@mathinput/element <math-input>, keypad, mathinput.css, tailwind.css.
@mathinput/react <MathInput> for React 18 and 19.
@mathinput/presets-uk Optional keypads for UK GCSE Foundation, GCSE Higher and A-level.

Theming MathInput

MathInput ships a default look that works in light and dark mode. Everything visual can be changed without touching its source, in increasing depth:

  1. Tokens — set --mi-* custom properties. Enough for most apps.
  2. Classes — style the public mi-* classes in your own CSS.
  3. Keypad — change keys, tabs and labels (see keypad-config.md).
  4. Replace the stylesheet — skip mathinput.css and style the class contract from scratch.

Loading the styles

import "@mathinput/element/define";          // registers <math-input>
import "@mathinput/element/mathinput.css";   // default styles

All default rules are inside @layer mathinput, so any ordinary (unlayered) rule of yours wins without !important.

With Tailwind CSS v4

Import MathInput's Tailwind entry before Tailwind:

@import "@mathinput/element/tailwind.css";
@import "tailwindcss";

This fixes the layer order (theme, base, mathinput, components, utilities) so Tailwind utilities override the defaults, and exposes the tokens as theme values: bg-mi-key, text-mi-field-fg, bg-mi-key-template, rounded-mi-key, font-mi-math and so on. Consumers without Tailwind never need it.

Tokens

Set tokens on math-input (or any ancestor):

math-input {
  --mi-field-border-focus: #7c3aed;
  --mi-key-template-bg: #f3e8ff;
  --mi-key-template-fg: #5b21b6;
  --mi-key-variant-indicator: #db2777;
  --mi-key-radius: 6px;
}
Group Tokens
Fonts --mi-font-ui, --mi-font-math (or the math-font attribute)
Field --mi-field-bg, --mi-field-fg, --mi-field-border, --mi-field-border-focus, --mi-field-ring, --mi-field-radius, --mi-field-size, --mi-field-min-height, --mi-field-padding, --mi-muted
Editing --mi-caret, --mi-row-active-bg, --mi-selection-bg, --mi-placeholder-border, --mi-placeholder-border-active, --mi-placeholder-bg-active, --mi-ghost-opacity, --mi-ghost-color, --mi-danger
Keypad --mi-keypad-bg, --mi-keypad-border, --mi-keypad-gap, --mi-keypad-radius
Keys --mi-key-bg, --mi-key-fg, --mi-key-shadow, --mi-key-radius, --mi-key-font-size, --mi-key-size-phone, --mi-key-size-tablet, --mi-key-size-desktop, --mi-key-operator-bg, --mi-key-template-bg, --mi-key-template-fg, --mi-key-primary-bg, --mi-key-primary-fg, --mi-key-pressed-bg
Key labels --mi-key-placeholder (boxes), --mi-key-placeholder-active (the box the caret lands in)
More options --mi-key-variant-indicator
Tabs, sheets --mi-tab-fg, --mi-tab-selected-bg, --mi-tab-selected-fg, --mi-sheet-bg, --mi-sheet-shadow
Motion --mi-motion (reduced-motion is respected automatically)

Keep text at 4.5:1 contrast and boxes and indicators at 3:1; the default tokens are checked in CI.

Light and dark

theme="auto" (default) follows the operating system; theme="light" or theme="dark" forces one. To follow your app's own theme switch, set the attribute from your theme state, or define your own token values under your theme selector.

The "more options" indicator

Keys with extra options (hold, right-click, or Alt+↓) show a filled corner in --mi-key-variant-indicator. Change the shape with data-variant-indicator="dot" or "bar" on the element.

Classes

Structural classes are public API and are listed in the spec (§10.2); for example .mi-field, .mi-key[data-kind="template"], .mi-frac__bar, .mi-fence__side--ghost. State is exposed as attributes on the element: data-focused, data-readonly, data-disabled, data-form (phone|tablet|desktop), data-keypad (open|closed), data-subject.

Try the skins

Pick a skin to restyle the field below. The CSS it needs is shown underneath; copy it into your own stylesheet.

Configuring the keypad

Attributes

Attribute Values Effect
subject maths · chemistry · physics Picks the preset and the typing rules.
keypad auto (default) · always · never · collapsed auto opens on touch devices and sits behind a toggle with a mouse.
keypad-container element id Renders the keypad inside your own element, for a bottom sheet or side panel.
submit-on-enter boolean Enter and the ↵ key fire submit. Without it the ↵ key becomes "next box".

el.keypadOpen = true opens it from code; the keypad-toggle event reports changes.

Each subject's keypad offers every key it has. To fit it to a course, remove keys by topic or by id.

Changing keys

Patch the preset:

el.keypadLayout = {
  removeTabs: ["letters"],
  removeKeys: ["letter-t", "const-pi"],
  addKeys: {
    algebra: [{ id: "k", label: { text: "k" }, aria: "k", action: { type: "k" } }],
  },
};

A patch always starts from the preset for the field's subject. Removed keys also disappear from long-press menus, and a tab left empty is dropped.

Or replace it entirely with a full KeypadLayout ({ numberPad, tabs, navigation }); start from keypadPreset(subject) in @mathinput/core.

Key ids are stable. List them with:

import { keypadPreset } from "@mathinput/core";
const l = keypadPreset("maths");
console.log([l.numberPad, ...l.tabs].flatMap((t) => t.keys.map((k) => k.id)));

Removing topics

Keys for more advanced topics carry tags. Remove a whole topic with removeTags:

el.keypadLayout = { removeTags: ["calculus", "logarithms"] };
Tag Keys
column-vectors column vector
logarithms ln, log, log to a base (the chemistry log key for pH is untagged)
exponentials e, eˣ
infinity ∞
calculus d/dx (with dy/dx), ∫
series Σ
vector-notation vector arrow, hat
reciprocal-trig sec, cosec, cot
proof ∴, ≡

Your own keys can carry tags too (tags: ["my-topic"]).

Curriculum presets

Ready-made key sets for a curriculum are optional packages built on the same patches. @mathinput/presets-uk covers GCSE Foundation, GCSE Higher and A-level in England:

npm install @mathinput/presets-uk
import { ukKeypadPatch } from "@mathinput/presets-uk";

el.keypadLayout = ukKeypadPatch("gcse-higher");
// Combine with your own changes:
el.keypadLayout = { ...ukKeypadPatch("gcse-foundation"), removeTabs: ["letters"] };
Level Hides
gcse-foundation everything gcse-higher hides, plus column vectors
gcse-higher logarithms, e and eˣ, ∞, calculus, Σ, vector arrows and hats, sec cosec cot, ∴, ≡
a-level nothing

ukKeypad(subject, level) returns the full trimmed layout, for example to list its key ids. In React, pass the patch as keypadLayout.

Key definitions

interface Key {
  id: string;
  aria: string;                      // accessible name, e.g. "fraction"
  label: { text: string }            // plain text
       | { tree: Row, active?: Position }  // a mini expression, drawn like the field
       | { icon: "left" | "right" | "backspace" | "enter" | "shift" | "keypad" | "table" }
       | { html: string };           // your own markup (you are responsible for it)
  action: { type: string }           // as if typed on a keyboard (subject rules apply)
        | { insert: Node | Node[] }
        | { template: Template, absorb?: boolean, prefix?: Node[] }
        | { command: CommandName }   // e.g. "moveLeft", "undo"
        | { bracket: "open" | "close" | "wrap", char: "(" | "[" | "{" | "|" }
        | { ui: "submit" | "shift" | "periodic-table" | "keypad-toggle" };
  variants?: Key[];                  // shown on long press; the key gets the indicator
  kind?: "digit" | "operator" | "template" | "letter" | "function" | "nav" | "primary" | "word";
}

Use tree labels for template keys so the key shows exactly what it inserts, with active marking the box the caret will land in. The tree builders in @mathinput/core (sup, frac, sqrt, …) make this short:

import { frac, sup } from "@mathinput/core";
const cubed = { id: "cubed", aria: "cubed", label: { tree: [sup("3")] }, action: { template: sup("3") } };

Output formats

Every event (input, change, submit) carries a value with:

Field Example (x = ½ or x = 3) Use
doc { version: 1, subject: "maths", root: [...] } Store it; set it back with el.value = doc. The only lossless format.
latex x=\frac{1}{2}\text{ or }x=3 Render with KaTeX or MathJax, put in documents, exchange with other tools. Chemistry: \ce{…} (mhchem).
text x = 1/2 or x = 3 Plain storage, search, logs, input to tools that take linear syntax.
spoken x equals 1 over 2 or x equals 3 Screen readers and voice output (already used by the element).
mathml <math …>…</math> Native browser rendering, assistive technology, office documents.
isEmpty, hasPlaceholders, hasUnbalancedBrackets Decide whether the expression is complete.

Formats are computed when you first read them. Empty boxes appear as \square in LaTeX and ? in text, and brackets left open are closed in the output but reported in hasUnbalancedBrackets.

Recipes

Store and restore. Keep doc; it is the only lossless format.

save(JSON.stringify(event.detail.doc));
el.value = JSON.parse(saved);

Render somewhere else. Wrap latex in your renderer's delimiters (MathInput never adds them). KaTeX needs the mhchem extension for chemistry.

katex.render(event.detail.latex, target);

Show it read-only. A readonly field renders any stored expression and reads it out to screen readers: <math-input readonly latex="…">.

Search or index. Use text, which is stable and readable: x = 1/2 or x = 3.

Accessibility. Use spoken for a label or live region, or mathml where assistive technology reads MathML.

Language models. Send LaTeX in $…$, optionally with the text as a second reading:

const v = event.detail;
const prompt = `Expression: $${v.latex}$ (read as: ${v.text})`;

Reading formats back

@mathinput/core parses its own output and common variants:

import { fromLatex, fromText } from "@mathinput/core";
el.value = fromLatex("\\frac{x+1}{2}", "maths");
el.value = fromText("Mg + 2HCl -> MgCl2 + H2", "chemistry");

The latex attribute does the same for an initial value. Unsupported LaTeX raises ParseError (or a parse-error event from the element).

Without the element

All formats are pure functions in @mathinput/core, usable on a server: toLatex(doc), toText(doc), toSpoken(doc), toMathML(doc).