npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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.

Readme

Html-Renderer

npm version CI License: MIT

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 stylesheet prop 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 (&lt;p&gt;hello&lt;/p&gt; 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-renderer

Peer 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, the border family (shorthands, per-side widths/colors, border-style, border-radius including per-corner), width/height with min-/max- variants
  • Other: opacity, display (flex and none)
  • Colors: hex, rgb(), rgba(), hsl(), named (passed through to RN's color system)
  • Units: px only for now — em/rem are not resolved yet, and % works only on width/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 rowspan on tables (colspan works).
  • CSS units beyond pxem, rem not resolved yet; % only on width/height.
  • Forms<input>, <textarea>, <button>, <select> not yet rendered (planned for core, pure-JS via RN's TextInput / Pressable).
  • Stylesheet features — pseudo-classes (:first-child, :nth-child), attribute selectors ([type="text"]), and @media queries not yet supported.
  • Advanced CSS — transforms, flex/grid layout of HTML content.
  • Intrinsic table column widths — columns render equal-width (flex: 1); width on <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 start

License

MIT — see LICENSE.