MathField
math-fieldMathLive-powered formula input: type LaTeX visually, inject it into MathTextarea and QuestionAnswer, and grade with CAS equivalence.
Usage
Basic usage
Controlled LaTeX value (without $). The first frame is a skeleton; mathlive loads dynamically on the client.
\frac{a}{b}+\sqrt{c}
<MathField value={latex} onChange={setLatex} aria-label="Formula" />Inject into MathTextarea
Pass the component itself as visualEditor; MathTextarea gains a Visual input tab, and confirming still inserts $…$ at the caret.
Preview (same typesetting as the question display)
Given , find .
Red source in the preview means KaTeX cannot parse that formula. Check the command spelling.
<MathTextarea multiline value={value} onChange={setValue} visualEditor={MathField} />Formula keyboard for blanks with three-tier grading
Set the blankInput of QuestionAnswer to math; on submit gradeObjective runs literal, normalized, then createCasComparator in order.
const equivalent = await createCasComparator();
<QuestionAnswer question={q} value={v} onChange={setV} blankInput="math" mathField={MathField}
onSubmit={(a) => gradeObjective(q, a, { normalize: true, equivalent })} />Virtual keyboard policy
manual opens only from the toggle; off attaches no keyboard (policy manual with the toggle hidden), suited to desktop authoring.
\frac{a}{b}+\sqrt{c}
<MathField value={latex} onChange={setLatex} virtualKeyboard="off" />Disabled and read-only
Pass disabled for a submitted answer; pass readOnly to display a reference answer.
\frac{a}{b}+\sqrt{c}
\int_0^1 x\,dx
<MathField value={latex} onChange={setLatex} disabled />
<MathField value={latex} onChange={setLatex} readOnly />When to use
A student has to type $\frac{5}{6}$ into a blank, or a teacher authoring questions does not know how to write \sqrt{}: the user sees a calculator-like box, what they type is already LaTeX, and the value you receive goes straight into Formula for typesetting or into gradeObjective for grading. To insert a formula into a run of text, use MathTextarea and pass this component as its visualEditor; it wraps the result in $…$ and inserts at the caret. MathField itself never emits $.
For display only, use Formula.
Installation
mathlive is an optional peer; the component only works once it is installed:
pnpm add mathliveTo use createCasComparator, also install @cortex-js/compute-engine (the dependency mathlive pins; it is usually already in node_modules, and installing it explicitly keeps the bundler's resolution independent of hoisting):
pnpm add mathlive @cortex-js/compute-engineImport the fonts once (the root layout in Next, main.tsx in Vite). Missing fonts fall back to system fonts; nothing goes blank:
import "mathlive/fonts.css";Peer floors are mathlive >=0.110.0 and @cortex-js/compute-engine >=0.58.0: only the versions we tested. Between MathLive 0.9x and 0.10x the semantics of menuItems and the virtual keyboard policy changed, so a lower floor would promise untested combinations.
Import
import { MathField, createCasComparator } from "@hulianui/ui/math-field"It lives on its own subpath rather than in @hulianui/ui/math: MathLive plus the Compute Engine is a lazy chunk of several hundred KB, and only pages that really need visual input or CAS grading should pay for it. @hulianui/ui/math itself contains zero MathLive.
Props
MathFieldProps extends `MathFieldLikeProps`; the first six rows are that contract.
| Name | Type | Default | Description |
|---|---|---|---|
| value | string | - | LaTeX without $ |
| onChange | (latex: string) => void | - | Called on every keystroke |
| onSubmit | (latex: string) => void | - | Enter. MathTextarea wires it to "insert at the caret" |
| disabled | boolean | false | Locked (submitted / submitting) |
| aria-label | string | - | Accessible name, forwarded to <math-field> |
| className | string | - | Outer container |
| virtualKeyboard | "auto" | "manual" | "off" | "auto" | Virtual keyboard policy: auto opens on focus on touch devices, manual opens only from the toggle, off attaches no keyboard (policy manual with the toggle hidden) |
| keyboardLayouts | readonly unknown[] | - | Forwarded to window.mathVirtualKeyboard.layouts. The keyboard is a page-level singleton; the last mounted field wins |
| readOnly | boolean | false | Read-only: selectable and copyable, not editable |
| placeholder | string | - | Placeholder while empty |
Events
| Event | Argument | When |
|---|---|---|
| onChange | latex: string | Every keystroke (MathLive's input event) |
| onSubmit | latex: string | Enter (Shift+Enter does not trigger it) |
createCasComparator
function createCasComparator(): Promise<(a: string, b: string) => boolean>Uses the Compute Engine to decide whether two LaTeX strings are mathematically equivalent: \frac{1}{2} vs 0.5 and 2x+1 vs 1+2x are both true. The result is a synchronous comparator that feeds the equivalent option of gradeObjective (tier 3, consulted only when the literal and normalized tiers both disagree). $…$ / $$…$$ / \(…\) are stripped from both sides first (stripMathDelimiters); a parse failure, an empty string, or any exception yields false, because grading should rather miss a match than invent one.
It is async: the Compute Engine is not bundled with mathlive, so the first call runs import() and later calls reuse the same engine instance. Without @cortex-js/compute-engine it throws ComputeEngineUnavailableError with the install command in the message.
The server is the grading source of truth (see the gradeObjective section in the Formula docs): this comparator gives instant feedback and authoring self-checks; official scores come from the server.
SSR and loading
The component has three states, exposed as data-status on data-slot="math-field": loading / ready / unavailable.
- loading: the server and the first client frame both render only a
Skeletonof the same size, so the HTML matches and there is no hydration mismatch. - ready: once
import("mathlive")inuseEffectsucceeds, the real element is created withdocument.createElement("math-field")and kept in sync with the controlled value. - unavailable: mathlive is missing, or the resolver picked MathLive's SSR build (no
MathfieldElement). AnAlertwith the install command is rendered, nothing throws, and a static export never fails because of it.warnOncefires once in development.
Next App Router works out of the box; the component is already a client component and needs no next/dynamic. In Vite you may add optimizeDeps.include: ["mathlive", "@cortex-js/compute-engine"] to avoid a mid-session re-optimization the first time it opens.
Virtual keyboard
MathLive's virtual keyboard is a page-level singleton (window.mathVirtualKeyboard); every MathField on the page shares it, and keyboardLayouts, when given, is written into that singleton, so the last mounted field wins. Desktop authoring usually sets virtualKeyboard="off"; touch-screen answering keeps the default auto.
Theming
MathLive reads its colors from CSS variables; the component pins them to Hulian tokens so light and dark follow the theme:
| MathLive variable | Hulian token |
|---|---|
--caret-color | --color-primary |
--selection-background-color | --color-primary at 18% |
--selection-color / --latex-color / --highlight-text | --color-foreground |
--contains-highlight-background-color | --color-primary at 10% |
--placeholder-color / --smart-fence-color | --color-muted-foreground |
--correct-color / --incorrect-color | --color-success / --color-danger |
The frame shares the border and focus ring of Input; MathLive's built-in context menu is disabled (menuItems = []).
Localization
The only copy is the loading placeholder's accessible name and the missing-dependency hint, read from components.mathField of ConfigProvider; the source of truth is math-field.locale.ts (MATH_FIELD_LOCALE_ZH / MATH_FIELD_LOCALE_EN).
Pitfalls
- The value is LaTeX without `$`. To insert it into a stem, go through the
visualEditorof MathTextarea (which adds the$); concatenatingvalueinto a stem yourself yields bare notation without delimiters. - Inject the component, not an element:
mathField={MathField}andvisualEditor={MathField}, never<MathField />. - In jsdom, mathlive resolves to its SSR build and the component shows the install hint: in consumer unit tests,
vi.mock("mathlive")with a fake element that implements onlygetValue/setValue, or assert the first-frame skeleton only. - MathLive's `setValue` requires a mounted element; the component appends first and writes afterwards. Do the same when you drive
<math-field>yourself.
Related
- MathTextarea: the
MathFieldLikePropscontract and thevisualEditorinjection point - QuestionAnswer:
blankInput="math"plusmathField - QuestionEditor: forwards
visualEditorto every MathTextarea - Formula: typesetting and the question-domain functions of
@hulianui/ui/math(gradeObjective) - Consuming guide for the math components: what each entry costs, SSR, grading source of truth
Playground
\frac{a}{b}+\sqrt{c}
<MathField placeholder="Enter a formula" aria-label="Formula" value={latex} onChange={setLatex} />