Skip to content

Instantly share code, notes, and snippets.

@hrdyjan1
Created September 9, 2025 07:07
Show Gist options
  • Select an option

  • Save hrdyjan1/4c96d63d3adee44deb93eca15a664964 to your computer and use it in GitHub Desktop.

Select an option

Save hrdyjan1/4c96d63d3adee44deb93eca15a664964 to your computer and use it in GitHub Desktop.
react-native-own-katex.tsx
import React, { memo, useMemo, useRef, useState } from 'react';
import { Platform, StyleProp, View, ViewStyle } from 'react-native';
import { WebView, WebViewMessageEvent } from 'react-native-webview';
interface RNKaTeXProps {
math: string;
displayMode?: boolean;
fontSize?: number;
macros?: Record<string, string>;
transparent?: boolean;
style?: StyleProp<ViewStyle>;
minHeight?: number;
textColor?: string;
}
const KATEX_VERSION = '0.16.11';
export default memo(function RNKaTeX({
math,
displayMode = false,
fontSize = 18,
macros = {},
transparent = true,
textColor = '#000',
style,
minHeight = 8,
}: RNKaTeXProps) {
const [contentHeight, setContentHeight] = useState(minHeight);
const webRef = useRef<WebView>(null);
const html = useMemo(
() => `
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@${KATEX_VERSION}/dist/katex.min.css">
<style>
html, body { margin: 0; padding: 0; background: ${transparent ? 'transparent' : '#fff'}; }
#root { ${displayMode ? 'display:block;' : 'display:inline-block;'} font-size: ${fontSize}px; color: ${textColor}; contain: content; }
</style>
</head>
<body>
<div id="root"></div>
<script src="https://cdn.jsdelivr.net/npm/katex@${KATEX_VERSION}/dist/katex.min.js"></script>
<script>
(function () {
// keep backslashes intact
const math = ${JSON.stringify(math)};
const macros = ${JSON.stringify(macros)};
const displayMode = ${displayMode ? 'true' : 'false'};
function render() {
try {
katex.render(math, document.getElementById('root'), {
displayMode, throwOnError:false, trust:true, strict:'ignore', macros
});
} catch (e) { document.getElementById('root').textContent = String(e); }
postHeight();
}
let lastH = -1, rafId = 0, tId = 0;
function measureH() {
const r = document.getElementById('root');
const h = Math.ceil(r.getBoundingClientRect().height);
return h | 0;
}
function postHeight() {
cancelAnimationFrame(rafId); clearTimeout(tId);
rafId = requestAnimationFrame(() => {
tId = setTimeout(() => {
const h = measureH();
if (Math.abs(h - lastH) >= 1) {
lastH = h;
window.ReactNativeWebView?.postMessage(JSON.stringify({type:'height', h}));
}
}, 50);
});
}
const ro = new ResizeObserver(postHeight);
ro.observe(document.getElementById('root'));
render();
window.addEventListener('load', () => setTimeout(postHeight, 60));
setTimeout(postHeight, 120); // fallback
})();
</script>
</body>
</html>
`,
[math, macros, displayMode, fontSize, textColor, transparent]
);
const onMessage = (e: WebViewMessageEvent) => {
try {
const data = JSON.parse(e.nativeEvent.data);
if (data?.type === 'height')
setContentHeight(Math.max(minHeight, data.h | 0));
} catch {}
};
// Software layer for block math on Android reduces shimmer
const androidProps =
Platform.OS === 'android' && displayMode
? {
androidHardwareAccelerationDisabled: true as const,
androidLayerType: 'software' as const,
}
: {};
return (
<View style={[{ minHeight: contentHeight }, style]}>
<WebView
ref={webRef}
originWhitelist={['*']}
onMessage={onMessage}
source={{ html }}
style={{ backgroundColor: 'transparent', height: contentHeight }}
scrollEnabled={false}
showsVerticalScrollIndicator={false}
showsHorizontalScrollIndicator={false}
{...androidProps}
/>
</View>
);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment