Library / SDK
pveyes/htmr avatar
pveyes/htmr

htmr converts an HTML string into React elements in under two kilobytes

Simple and lightweight (< 2kB) HTML string to React element conversion library

521 stars31 forksTypeScriptMIT

At a glance

What is it?
htmr is a small TypeScript library that parses an HTML string and returns React elements instead of injecting raw markup, with optional tag-to-component mapping, attribute preservation and control over which tags get dangerouslySetInnerHTML. It suits a developer rendering HTML that came from an API, and the author is explicit that file size and a simple API were chosen over full HTML coverage.
Who is it for?
htmr fits a developer rendering HTML that arrived from an API into a React tree, especially one who wants to map tags onto their own components rather than injecting markup, and the sub-2kB footprint makes it a cheap dependency. It does not fit a project that needs a complete HTML-to-React conversion, because the author states that coverage, JSX parsing and node traversal were deliberately left out.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 17 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What it does and how small it is

htmr is a simple and lightweight HTML string to React element conversion library, and the size constraint is the headline: less than 2kB.

The README badge measures the gzipped bundle from unpkg, so the number refers to the compressed browser build rather than the source. The package description puts it as a lightweight HTML to React converter that works in server and browser, and the keywords list server, browser and universal.

The usage is a single call.

js
import React from 'react';
import htmr from 'htmr';

function HTMLComponent() {
  return htmr('<p>No more dangerouslySetInnerHTML</p>');
}

So the library takes an HTML string and returns React elements rather than a blob of markup you inject. The difference from dangerouslySetInnerHTML is not cosmetic. With innerHTML the browser owns the DOM inside the container, so React cannot reconcile it, and any styling or behaviour you expected from your component tree does not apply. With htmr you get elements, which means your CSS applies, your components can be substituted, and React manages the subtree.

The implementation explains the two dependencies. htmlparser2 is the parser used on the server, and html-entities handles entity decoding. In the browser the README notes that htmr uses the native browser HTML parser rather than a custom one, which is where much of the size saving comes from.

The three options and their defaults

The API accepts a second argument, options, with a small set of fields and documented defaults.

js
const options = {
  transform: {},
  preserveAttributes: [],
  dangerouslySetChildren: ['style'],
};
htmr(html, options);

The transform map lets you substitute a tag name with a component, so you control how particular elements are rendered. The preserveAttributes list lets you keep HTML attribute names as they are rather than camelCasing them, and it accepts strings or regular expressions. The dangerouslySetChildren list names the tags whose children are rendered as raw HTML.

That last default is the one worth reading carefully. By default htmr renders the children of a style tag inside dangerouslySetInnerHTML, and nothing else. The reason given is security, and the rule for extending it is that style must still be listed explicitly if you want it to keep that behaviour.

So the default posture is: no script tag content is ever injected, style content is, and everything else is parsed into React elements. That is a defensible default for a library whose input may come from somewhere you do not control.

Transform, and the case for mapping tags onto your components

The transform option is the reason to reach for this library rather than parsing the HTML yourself.

js
import React from 'react';
import htmr from 'htmr';
import styled from 'styled-components';

const Paragraph = styled.p`
  font-family: Helvetica, Arial, sans-serif;
  line-height: 1.5;
`;

const transform = {
  p: Paragraph,
  // you can also pass string for native DOM node
  a: 'span',
};

function TransformedHTMLComponent() {
  // will return <Paragraph><span>{'Custom component'}</span></Paragraph>
  return htmr('<p><a>Custom component</a></p>', { transform });
}

The mapping is by tag name, and the value can be either a component or a string for a native DOM node. The comment in the example shows the result, where the p becomes the styled Paragraph and the a becomes a span.

There is also a default transform, given under the `_` property name, which is the more interesting mechanism.

js
import React from 'react';
import { Text, View } from 'react-native';

const transform = {
  div: View,
  _: (node, props, children) => {
    // react-native can't render string without <Text> component
    // we can test text node by checking component props, text node won't have them
    if (typeof props === 'undefined') {
      // use `key` because it's possible that <Text> is rendered
      // inside array as sibling
      return <Text key={node}>{node}</Text>;
    }

    // render unknown tag using <View>
    // ideally you also filter valid props to <View />
    return <View {...props}>{children}</View>;
  },
};

function NativeHTMLRenderer(props) {
  return htmr(props.html, { transform });
}

The `_` entry catches every tag that has no specific mapping, and it receives the node, the props and the children. The technique shown here is a neat one: react-native cannot render a bare string without a Text wrapper, so the fallback distinguishes text nodes from element nodes by checking whether props is undefined, and wraps only the text ones.

That gives react-native the same HTML-to-element conversion with a small transform, which is the package's react-native export field in package.json points at.

Attribute handling, dangerous children and multiple roots

Two smaller behaviours are documented separately because they surprise people.

By default htmr converts HTML attributes to camelCase because that is what React uses. You can override this with preserveAttributes, giving an array of strings or regular expressions to test against, for example `htmr(html, { preserveAttributes: ['ng-if', new RegExp('v-')] })` to keep ng-if, v-if and v-for as they are. The stated purpose is rendering HTML that carries Angular or Vue directives.

The dangerouslySetChildren option extends the set of tags whose children are injected raw: `htmr(html, { dangerouslySetChildren: ['code', 'style'] })`. The note is that if you still want style rendered using dangerouslySetInnerHTML you still need to include it in the array, because the option replaces the default rather than adding to it.

Multiple top-level elements are supported, and the return value is an array in that case.

js
import React from 'react';
import htmr from 'htmr';

const html = `
  <h1>This string</h1>
  <p>Contains multiple html tags</p>
  <p>as sibling</p>
`;

function ComponentWithSibling() {
  // if using react 16, simply use the return value because
  // v16 can render array
  return htmr(html);
  // if using react 15 and below, wrap in another component
  return <div>{htmr(html)}</div>;
}

The React version note matters if you are on an older React, and the peer dependency range is react >=15.6.1, so the library itself supports versions well before React 16 even though the array-returning convenience starts at 16.

The trade-offs, stated by the author

The README has a Trade Off section, and it is the part to read before adopting the library.

Inline event attributes such as onclick are not supported, due to unnecessary complexity.

In the browser, htmr uses the native browser HTML parser instead of a custom parser. Due to how the browser HTML parser works, you can get a weird result if you supply invalid HTML, and the example given is a div inside a p element, which the browser will restructure rather than nest.

The script tag is not rendered using dangerouslySetInnerHTML by default, for security reasons, and you can opt in with the dangerouslySetChildren option. The style tag does render its children using dangerouslySetInnerHTML by default, and you can reverse that with the same method.

The author's framing of the design is explicit: this library prioritises file size and a simple API over full HTML conversion coverage and other features like JSX parsing or flexible node traversal. That is why some features were not implemented, and the README suggests related projects if you need more.

Those alternatives are listed too. For HTML to React elements there is html-to-react, react-html-parser, html-react-parser and react-render-html. For converting an HTML page to a React component in file or string form there is html-to-react-components and react-magic.

So the trade is explicit: htmr is under 2kB and covers the common cases, while the larger alternatives cover more of the specification and handle malformed markup differently.

The repository, and how the size budget is enforced

The project layout shows a library with a size constraint treated as a test.

There is src/ for the source, test/ for the tests, a benchmark/ directory, a .codesandbox/ directory for examples, index.d.ts and src/types.ts for the type definitions, and a rollup.config.js for the build. The .babelrc and tsconfig.json handle the transpilation, .prettierrc the formatting and .husky/ the git hooks.

The package.json scripts show the size budget being enforced in CI. The test script is `npm-run-all clean build && npm-run-all -p test:coverage test:size typecheck bench`, so running the full test suite does four things in parallel: coverage with jest, a size check with bundlesize, a typecheck with tsc, and the benchmark. There is a separate bundlesize step, which is what fails the build if the bundle grows past the budget.

That is a rare and good practice. A library whose main selling point is its size will lose that selling point silently, and the only defence is a check in the build.

The package has two runtime dependencies, html-entities and htmlparser2, and React as a peer dependency at >=15.6.1, which is why htmr does not bring React along with it.

The release history shows a long gap and then a burst. The listed releases are v1.1.1 and v1.1.0, both from 2026-10-06, and v1.0.2 from 2022-08-11. So the project spent four years between releases and then shipped two in a day, with the last push to the repository also on 2026-10-06.

Where htmr is the wrong choice

Four cases argue against it.

The first is a project that needs real HTML coverage. The author says outright that full HTML conversion coverage was traded away, along with JSX parsing and flexible node traversal. If your input is arbitrary HTML from an untrusted source with unusual structure, or you need to walk the resulting tree and pull out specific elements, this is the wrong tool.

The second is a project that needs to preserve invalid or unusual markup structure in the browser. Since the browser's own parser is used there, a div inside a p will be restructured by the browser, and the React tree you get back will not match the string you passed in. That difference is invisible until it matters.

The third is a project where the HTML is trusted and simple. If you are rendering your own markup, a template literal in JSX or a component per element is clearer than parsing and converting, and there is no dependency to justify.

The alternative in the same direction is one of the larger HTML-to-React libraries the README lists, or simply not converting at all. The difference in approach is that htmr gives you a React element tree from an HTML string with a small bundle and a transform hook, while the larger libraries handle more of the specification and give you more control over the result, at the cost of size. If your HTML comes from a CMS or an email template and you want it to look like the rest of your app, htmr plus a transform map is the efficient answer. If your HTML is arbitrary user content and you need to sanitise it and manipulate the tree, you want a bigger library and probably a sanitiser anyway.

Editorial conclusion

htmr fits a developer rendering HTML that arrived from an API into a React tree, especially one who wants to map tags onto their own components rather than injecting markup, and the sub-2kB footprint makes it a cheap dependency. It does not fit a project that needs a complete HTML-to-React conversion, because the author states that coverage, JSX parsing and node traversal were deliberately left out. Install it with `npm install htmr --save`, pass the HTML string to the default export, and read the Trade Off section before you rely on it, since inline event attributes are not supported and the browser's own parser can restructure invalid markup.

Frequently asked questions

What does htmr do?

It converts an HTML string into React elements rather than injecting raw markup, so the result is a React tree you can style and reconcile. The library is under 2kB gzipped, uses the native browser parser in the browser and htmlparser2 on the server, and works in React Native through the react-native export.

How do I install and use htmr?

Run `npm install htmr --save`, then import the default export and pass the HTML string to it, for example `htmr('<p>No more dangerouslySetInnerHTML</p>')`. An options object as the second argument controls transforms, preserved attributes and dangerous children.

What are the htmr options and their defaults?

The defaults are an empty transform map, an empty preserveAttributes array, and dangerouslySetChildren set to ['style']. The transform map substitutes tag names with components, preserveAttributes keeps selected HTML attribute names un-camelCased, and dangerouslySetChildren names the tags whose children are rendered as raw HTML.

Can htmr render HTML in React Native?

Yes, through a transform with a default `_` entry that catches unmapped tags. The README shows mapping div to View and using the default function to wrap text nodes in Text, since React Native cannot render a bare string without it.

What are htmr's documented limitations?

Inline event attributes such as onclick are not supported. In the browser the native HTML parser is used, so invalid markup like a div inside a p can produce a restructured result. The script tag is never rendered as raw HTML by default, and the library prioritises file size and a simple API over full HTML conversion coverage, JSX parsing and node traversal.

Official sources

  1. Issues
  2. License: MIT
  3. pveyes/htmr on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/pveyes-htmr.svg)](https://hysenlabs.com/projects/pveyes-htmr)

Was this useful?