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:
- Tokens — set
--mi-*custom properties. Enough for most apps. - Classes — style the public
mi-*classes in your own CSS. - Keypad — change keys, tabs and labels (see keypad-config.md).
- Replace the stylesheet — skip
mathinput.cssand 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).