ReactでKaTeXを使って、数式をきれいに表示する方法
KaTeX は、\frac{1}{2} のような TeX の式を、ブラウザで表示できる HTML に変えるライブラリです。React では、KaTeX が返す HTML の文字列を、小さなコンポーネントで包んで表示できます。
文中の数式とブロックの数式を出すコンポーネントを TypeScript で書き、Chromium で動かしました。CSS を読み込まなかったときの崩れ方、書き間違えた式の扱い、利用者が入力した式の表示、スマートフォンの幅での横に長い式、読み込むファイルの大きさを確かめています。
検証環境は Ubuntu 24.04、Node.js 22.23.4、Chromium 153 です。使ったのは katex 0.19.1、react / react-dom 19.3.1、typescript 7.0.2、esbuild 0.28.2、Playwright 1.63.0 です。.tsx を tsc で型チェックしてから esbuild でまとめ、表示の幅 375px で確かめました。esbuild 以外のバンドラー、Firefox と Safari、スマートフォンの実機、スクリーンリーダーでの読み上げ、サーバーでの描画(SSR や Server Components)、Markdown の中の数式を表示する方法は試していません。
KaTeX を入れて、CSS も読み込む
npm install katex
katex 0.19.1 には型定義が同梱されていて、@types/katex を足さなくても tsc の型チェックが通りました。
KaTeX は、JavaScript だけでは数式を正しく表示できません。CSS の katex.min.css と、そこから読み込まれる専用のフォントが必要です。アプリの入り口で CSS を読み込みます。
// main.tsx
import 'katex/dist/katex.min.css';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './App';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);
katex.min.css の中では、フォントのファイルが url(fonts/KaTeX_Main-Regular.woff2) のような相対パスで書かれています。esbuild でまとめるときは、--loader:.woff2=file --loader:.woff=file --loader:.ttf=file を付けてフォントをファイルとして出力させました。
CSS を読み込まないと、どうなるかも試しました。下の画像の上の段が、CSS を読み込まずに二次方程式の解の公式を表示したところです。

CSS がないと、同じ式が2回出ます。画像の上の段では、上にある分数の形の式が、KaTeX が一緒に出力する MathML をブラウザがそのまま描いたものです。その下の崩れた文字と根号の線は、見た目用の HTML が崩れたものです。CSS を読み込むと、MathML の部分は position: absolute、幅 1px になり、画面に出る式は1つになります。式が崩れたり二重に出たりしたときは、まず CSS が読み込まれているかを確かめてください。公式ドキュメントのよくある問題のページにも、CSS が読み込まれているかを確かめる方法が載っています。
フォントは、ビルドすると woff2・woff・ttf が 20 個ずつ、合わせて 60 個出力されました。ブラウザが読み込んだのは、そのうちの woff2 の一部だけです。文中とブロックの数式、横に長い式、入力欄、\href や HTML のタグを含む入力を並べた検証用のページでは、KaTeX_Main-Regular、KaTeX_Math-Italic、KaTeX_Size1-Regular、KaTeX_Size2-Regular の4つでした。
数式を表示するコンポーネントを書く
katex.renderToString は、TeX の式を受け取り、HTML の文字列を返します(公式ドキュメントの API の説明)。それを dangerouslySetInnerHTML で入れるコンポーネントにしました。
// Tex.tsx
import katex from 'katex';
import { useMemo } from 'react';
type Props = { tex: string; display?: boolean };
type Result = { html: string } | { error: string };
export function Tex({ tex, display = false }: Props) {
const result = useMemo<Result>(() => {
try {
return { html: katex.renderToString(tex, { displayMode: display }) };
} catch (e) {
if (e instanceof katex.ParseError) return { error: e.message };
throw e;
}
}, [tex, display]);
if ('error' in result) {
return (
<code className="tex-error" title={result.error}>
{tex}
</code>
);
}
const Tag = display ? 'div' : 'span';
return <Tag dangerouslySetInnerHTML={{ __html: result.html }} />;
}
useMemo で、tex と display が変わったときだけ HTML を作り直します。書き間違いのときの try / catch は、次の節で説明します。
使うときは、文の中なら display を付けずに、独立した行に大きく出すなら display を付けます。
<p>
円の面積は <Tex tex={String.raw`\pi r^2`} /> で求められます。
</p>
<Tex display tex={String.raw`x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}`} />
TeX の式はバックスラッシュを多く使います。普通の文字列では "\\frac" のように2つ重ねる必要がありますが、String.raw のテンプレートリテラルなら、そのまま書けます。この書き方は、公式ドキュメントの API のページでも紹介されています。ただし、String.raw を使うと ${ とバッククォートをエスケープできないことも、同じページに注意として書かれています。
検証環境で表示すると、文の中の式は <span class="katex"> になり、文字の大きさはまわりの 16px に対して 19.36px(1.21倍)でした。公式ドキュメントのフォントのページには、上付き・下付きの文字を読みやすくするため 1.21 倍で表示すること、.katex { font-size: 1.1em; } のような CSS で変えられることが書かれています。
display を付けた式は <span class="katex-display"> で包まれ、中央に寄せて表示されました。同じ \sum_{k=1}^{n} k = \frac{n(n+1)}{2} の式でも、文の中では高さ 22px、ブロックでは 57.2px でした。オプションの説明にあるとおり、ブロックの数式では \sum の上下に添え字が付き、記号も大きくなります。
KaTeX は、見た目用の HTML と一緒に MathML も出力します(output オプションの既定値 htmlAndMathml)。公式ドキュメントでは、MathML はアクセシビリティのために入れているとされています。出力された HTML では、見た目用の部分に aria-hidden="true" が付いていました。
書き間違いは、元の式のまま赤く出す
KaTeX は、式を読み取れないと katex.ParseError を投げます(エラーの扱いの説明)。閉じかっこが1つ足りない \frac{1}{2 を渡すと、次のメッセージで例外になりました。
KaTeX parse error: Unexpected end of input in a macro argument, expected '}' at end of input: \frac{1}{2
React のコンポーネントの描画中に例外が出ると、Error Boundary で受け止めない限り、画面の表示が消えます(React の公式リファレンスの Error Boundary の説明)。そこで Tex では ParseError だけを受け止め、元の式を <code className="tex-error"> で出し、メッセージは title 属性に入れました。title はマウスを重ねたときに出るものなので、スマートフォンでは見えません。赤い文字にするのは、次の CSS です。
.tex-error {
color: #cc0000;
}
<code> の中なので、ブラウザの既定の表示で等幅の文字になります。
入力欄に打った式をその場で表示するページで試したところ、書きかけの式は赤い文字のまま表示され、閉じかっこを足した時点で数式に変わりました。ブラウザのコンソールにエラーは出ていません。

公式ドキュメントでは、エラーのメッセージには TeX の式の一部がそのまま入るため、HTML に入れるときは & や < をエスケープするよう注意しています。Tex では、式もメッセージも JSX の子や属性として渡していて、HTML として入れてはいません。入力欄に \frac{<img src=x onerror=alert(1)> と書きかけの式を入れたところ、その文字列が赤い文字のまま表示され、ページに <img> 要素は作られませんでした。
同じことは、throwOnError: false のオプションでもできます。この場合は例外にならず、式がそのまま #cc0000 の色の <span class="katex-error"> で出力され、メッセージは title 属性に入りました。ただ、\fracc{1}{2} のように知らない命令が入っているときは、エラーにはならず、\fracc だけを赤くして、残りの {1}{2} は 12 という数字として出力しました(見た目用の HTML の文字は \fracc12)。書き間違いをはっきり見せたいときは、ParseError を受け止める書き方のほうが分かりやすいです。
利用者が書いた式を表示するときの注意
コメント欄のように、利用者が書いた式を表示することもあります。dangerouslySetInnerHTML を使うので、気になるのは HTML を差し込まれないかです。公式ドキュメントのセキュリティのページでは、KaTeX が作る HTML は <script> などを差し込まれないものになっている、とされています。
<img src=x onerror=alert(1)> という文字列を Tex に渡したところ、< や = は数式の記号として並べられ、ページに <img> 要素は作られませんでした。alert のダイアログも開いていません。
リンクを作る \href のように、外部のファイルを読み込んだり HTML の属性を変えたりする命令は、trust オプションで許可しない限り使えません。既定のまま \href{javascript:alert(1)}{\text{click}} を表示すると、リンクは作られず、\href という赤い文字だけが出ました。リンクを使いたいときは、trust に関数を渡して、許可する命令とプロトコルを絞れます。
import katex from 'katex';
export function renderWithLinks(tex: string): string {
return katex.renderToString(tex, {
trust: (ctx) => ctx.command === '\\href' && ['http', 'https'].includes(ctx.protocol ?? ''),
});
}
型定義では ctx.protocol が省略できる値(string | undefined)なので、?? '' を付けないと tsc の型チェックが通りません。この関数を渡すと、https://example.com/ へのリンクは <a href="https://example.com/"> になり、javascript: のリンクは赤い \href のままでした。trust: true にすると、javascript:alert(1) のリンクもそのまま作られました。利用者が書いた式に trust: true を使うのは避けてください。
ほかに、セキュリティのページでは maxSize と maxExpand のオプションが挙げられています。\def\a{\a}\a のように自分自身を呼ぶマクロは、既定の maxExpand(1000回)で止まり、Too many expansions の ParseError になりました。\rule{1000em}{1em} のような大きな線は、既定では幅 1000em のまま出力され、maxSize: 10 を付けると幅 10em に抑えられました。
スマートフォンで横に長い式をスクロールさせる
ブロックの数式は、KaTeX が自動では折り返しません。幅 375px の画面で (a+b)^5 を展開した長い式を表示すると、式の幅がはみ出して、ページ全体が横にスクロールする状態(ページの幅が 438px)になりました。
公式ドキュメントのよくある問題のページにある CSS を足すと、式の部分だけを横にスクロールさせられます。
.katex-display {
overflow: auto hidden;
}
この CSS を入れると、ページの幅は画面と同じ 375px に収まりました。式の枠の幅は 343px で、中身の幅は 422px あり、はみ出した分は枠の中で横にスクロールします。
読み込むファイルの大きさ
本番用ビルド(esbuild の minify あり)で、ブロックの数式を1つだけ表示するページを作り、ブラウザが読み込んだファイルを測りました。比べる相手は、同じ式を普通の文字で書いた、React だけのページです。gzip の大きさは、JS と CSS を Node.js の zlib(レベル 9)で圧縮した値で、woff2 はそのままの大きさを足しています。
| ページ | 読み込んだファイル | 大きさ | gzip |
|---|---|---|---|
| React だけ | JS 1つ | 223,504 バイト | 69,136 バイト |
| KaTeX で表示 | JS・CSS・woff2 2つ | 560,695 バイト | 192,736 バイト |
KaTeX を使うページの内訳は、JS が 492,782 バイト、CSS が 25,201 バイト、フォントが KaTeX_Main-Regular の 26,272 バイトと KaTeX_Math-Italic の 16,440 バイトでした。JS は、React だけのページより 269,278 バイト増えています。





