@nikpnevmatikos/html-renderer
v0.3.0
Published
React Native HTML renderer in TypeScript — zero native modules, Fabric/Expo compatible. Supports tagsStyles, stylesheet with CSS selectors, custom renderers, and more.
Maintainers
Readme
Html-Renderer
A modern React Native HTML renderer, written in TypeScript with zero native modules. Built from scratch as a maintained alternative to the abandoned react-native-render-html.
- Zero native code — works on iOS, Android, Web (via
react-native-web), and Expo Go without a dev build. - Fabric (new architecture) compatible out of the box.
- Real CSS stylesheet support — a
stylesheetprop that accepts actual CSS with selectors and specificity. - Transient render tree model: HTML → DOM → resolved render tree →
<Text>/<View>/<Image>. - Full style inheritance, CSS cascade, and the box-model basics.
- Extensible via custom renderers, custom element models, DOM transform hooks, and per-renderer config.
- Entity-encoded HTML (
<p>hello</p>from CMS/API backends, even double-encoded) is auto-detected and rendered as HTML — no pre-decoding needed. - 130+ unit tests, typed end-to-end.
Install
npm install @nikpnevmatikos/html-rendererPeer dependencies: react >= 18, react-native >= 0.73.
Quick start
import { HtmlRenderer } from '@nikpnevmatikos/html-renderer';
export default function Screen() {
return (
<HtmlRenderer
html={`<h1>Hello</h1><p>This is <strong>bold</strong> and <a href="https://x.dev">a link</a>.</p>`}
/>
);
}Props
| Prop | Type | Description |
|---|---|---|
| html | string | The HTML source to render. |
| baseStyle | ResolvedStyle | Root style. Inherited text props cascade to all content; box props (background, padding, …) style the root container. |
| stylesheet | string | A CSS stylesheet with real selectors (type, class, id, descendant, child). |
| tagsStyles | Record<tag, StyleInput> | Per-tag style override ({ h1: {...} }). body is special: it styles the document root even when the HTML is a fragment with no <body> tag — see below. |
| classesStyles | Record<class, StyleInput> | Style by class attribute. |
| idsStyles | Record<id, StyleInput> | Style by id attribute. |
| customRenderers | Record<tag, CustomRenderer> | Replace or wrap the renderer for any tag. |
| customHTMLElementModels | Record<tag, HTMLElementModel> | Define new tags with custom block/inline semantics and default styles. |
| renderersProps | Record<tag, Record<string, unknown>> | Per-renderer config. Built-in consumers: ol.startIndex, ul/ol.markerTextStyle, img.initialDimensions. |
| contentWidth | number | Max render width. Images wider than this scale down proportionally. |
| transformDom | (dom: DomNode[]) => DomNode[] | Runs after parse, before build. Use for sanitization or tag rewrites. |
| onLinkPress | (href, attribs) => void | Override the default Linking.openURL link handler. |
| ignoredDomTags | string[] | Tags to drop during parse (subtree removed). Merged with the built-in defaults: head, title, style, script, link, meta, base. |
| ignoredStyles | string[] | CSS properties to drop. Accepts kebab-case (background-color) or camelCase (backgroundColor). |
| defaultTextProps | TextProps | Spread onto every <Text>. |
| defaultViewProps | ViewProps | Spread onto every <View>. |
| textSelectable | boolean | Shortcut for defaultTextProps.selectable = true. |
StyleInput is ResolvedStyle | string — every style map accepts either an RN-style object or a CSS declarations string:
tagsStyles={{
h1: { color: 'red', fontSize: 24 },
h2: 'color: blue; font-size: 20px', // CSS string works too
}}Supported tags
Block: p, div, h1–h6, ul, ol, li, pre, blockquote, hr, table, thead, tbody, tfoot, tr, th, td, caption.
Inline: span, strong/b, em/i, u, s/del/strike, ins, mark, small, code, a, br, img.
Documents: full-document HTML works too — literal <html>/<body> render as plain block containers, and <head>, <title>, <style>, <script>, <link>, <meta>, <base> are ignored by default.
Media: <video> / <audio> render their spec fallback content as blocks and expose all attributes (attribs) to custom renderers; native playback ships as plugin packages (see below).
Supported CSS
- Typography:
color,font-size(px),font-family,font-weight,font-style,text-align,text-decoration,text-transform,letter-spacing,line-height(px) - Box model:
margin/padding(shorthands + individual sides),background-color, theborderfamily (shorthands, per-side widths/colors,border-style,border-radiusincluding per-corner),width/heightwithmin-/max-variants - Other:
opacity,display(flexandnone) - Colors: hex,
rgb(),rgba(),hsl(), named (passed through to RN's color system) - Units:
pxonly for now —em/remare not resolved yet, and%works only onwidth/height
Examples
Root styles — baseStyle and tagsStyles.body
tagsStyles.body styles the document root even when the source HTML is a fragment with no <body> element — as if the content were wrapped in a synthetic body. Inherited text properties cascade into all content; box properties are applied once to the root container. This makes it the natural way to set the root text color:
<HtmlRenderer
html="<p>Hello</p>"
tagsStyles={{ body: { color: 'white' } }} // text renders white
/>baseStyle does the same thing one rung lower in the cascade (tagsStyles.body wins where both set a property). If the HTML contains a literal <body> tag, tagsStyles.body is applied on that element instead, so box props are never applied twice.
stylesheet — real CSS with selectors
const css = `
article.card {
background-color: #fafbfc;
padding: 12px;
}
article.card h3 {
color: #1a73e8;
}
.highlight {
background-color: #fff3a3;
}
h1 > span {
font-weight: bold;
}
`;
<HtmlRenderer html={html} stylesheet={css} />;Supports: type (h1), class (.foo), id (#bar), universal (*), compound (h1.big#hero), descendant (article span), child (h1 > span), selector lists (h1, h2). Specificity and source order work per the CSS spec.
Not supported: pseudo-classes, pseudo-elements, attribute selectors, sibling combinators (+, ~), @media queries.
Custom renderer
import { type CustomRenderer } from '@nikpnevmatikos/html-renderer';
const customRenderers: Record<string, CustomRenderer> = {
h1: (node, defaultRender) => (
<View style={{ borderBottomWidth: 2, borderBottomColor: 'blue' }}>
{defaultRender()}
</View>
),
};
<HtmlRenderer html={html} customRenderers={customRenderers} />;Custom HTML element models
Define your own tags that behave like real HTML:
import { type HTMLElementModel } from '@nikpnevmatikos/html-renderer';
const customHTMLElementModels: Record<string, HTMLElementModel> = {
'my-card': {
display: 'block',
tagDefaultStyle: { backgroundColor: '#eef', padding: 12 },
},
'x-spacer': {
display: 'block',
isVoid: true, // ignore any children
tagDefaultStyle: { height: 20 },
},
};
<HtmlRenderer
html="<my-card>hello</my-card>"
customHTMLElementModels={customHTMLElementModels}
/>;DOM transform hook
import { type TransformDom } from '@nikpnevmatikos/html-renderer';
const sanitize: TransformDom = (dom) => walk(dom);
function walk(nodes) {
return nodes.map((n) => {
if (n.type === 'element') {
return { ...n, attribs: { ...n.attribs, onclick: '' }, children: walk(n.children) };
}
return n;
});
}
<HtmlRenderer html={html} transformDom={sanitize} />;Link handling
import { type OnLinkPress } from '@nikpnevmatikos/html-renderer';
const onLinkPress: OnLinkPress = (href, attribs) => {
if (attribs.target === '_blank') {
void Linking.openURL(href);
} else {
navigation.navigate('InAppBrowser', { url: href });
}
};
<HtmlRenderer html={html} onLinkPress={onLinkPress} />;Auto-fit images
import { Dimensions } from 'react-native';
const contentWidth = Dimensions.get('window').width - 32;
<HtmlRenderer html={html} contentWidth={contentWidth} />;Renderers props (per-renderer config)
<HtmlRenderer
html={html}
renderersProps={{
ol: { startIndex: 5, markerTextStyle: { color: '#888' } },
ul: { markerTextStyle: { color: 'red' } },
img: { initialDimensions: { width: 300, height: 200 } },
}}
/>Cascade order
From lowest to highest priority:
1. baseStyle (HtmlRenderer prop — root defaults)
2. tagsStyles.body (document root — applies even without a <body> tag)
3. Built-in tag defaults (h1 bold, strong bold, etc.)
4. stylesheet matches (by selector specificity + source order)
5. tagsStyles (per-tag programmatic override)
6. classesStyles (by matched class)
7. idsStyles (by matched id)
8. Inline style="..." (highest — HTML inline always wins)Root styles (1–2) reach descendants through inheritance, so only inherited text properties cascade down — and any element-level match (3–8) overrides them.
How it works
HTML string
└─ parseHtml (htmlparser2) → DOM tree
└─ transformDom? (optional) → DOM tree
└─ buildRenderTree → Render tree
└─ resolveStyles (full cascade per element)
└─ hoistBlocks (fragment inline-wrapping-block)
└─ collapseWhitespace (CSS whitespace rules)
└─ Renderer → <View> / <Text> / <Image>Styles resolve at build time into a single ResolvedStyle per element. At render time, splitStyle partitions each style into View-applicable and Text-applicable halves and applies them to the correct component.
Current limitations
Actively on the roadmap:
- No
rowspanon tables (colspan works). - CSS units beyond
px—em,remnot resolved yet;%only onwidth/height. - Forms —
<input>,<textarea>,<button>,<select>not yet rendered (planned for core, pure-JS via RN'sTextInput/Pressable). - Stylesheet features — pseudo-classes (
:first-child,:nth-child), attribute selectors ([type="text"]), and@mediaqueries not yet supported. - Advanced CSS — transforms, flex/grid layout of HTML content.
- Intrinsic table column widths — columns render equal-width (
flex: 1);widthon<col>/ cells is ignored.
Plugin packages (planned)
Features that need native dependencies ship as separate packages so the core stays zero-native-modules. Each uses the core's customRenderers and customHTMLElementModels APIs — no core changes needed to add them.
| Tags | Planned package | Native peer dep |
|---|---|---|
| <iframe> | @nikpnevmatikos/html-renderer-webview | react-native-webview |
| <video>, <audio> | @nikpnevmatikos/html-renderer-video | expo-video or react-native-video |
| <svg> | @nikpnevmatikos/html-renderer-svg | react-native-svg |
Until these ship, you can wire any of these tags yourself via customRenderers — the same API the plugins will use. See the "Custom renderer" and "Custom HTML element models" examples above.
Development
npm install # installs all workspace deps
npm run dev # tsc --watch on core
npm test # jest — 130+ tests
npm run typecheck # tsc --noEmit on core
npm run build # build core to dist
# live example app
cd example && npm startLicense
MIT — see LICENSE.
