npm package: @nanogiants/react-native-render-html
Description: A lightweight, customisable HTML renderer for React Native
Latest version: 1.0.6

# @nanogiants/react-native-render-html

A lightweight, customisable HTML renderer for React Native.

Parses an HTML string and maps each recognised element to the appropriate React Native primitive — no native modules, no WebView. Images and text components are injected via render props, keeping the package free of any hard image-library dependency.

---

## Installation

```sh
npm install @nanogiants/react-native-render-html
# or
yarn add @nanogiants/react-native-render-html
# or
pnpm add @nanogiants/react-native-render-html
```

`react` and `react-native` are peer dependencies and must already be present in your project.

---

## Quick start

```tsx
import { RenderHTML } from '@nanogiants/react-native-render-html';
import { Image } from 'react-native';

const html = `
  <h1>Hello world</h1>
  <p>This is a <strong>bold</strong> statement.</p>
  <img src="https://picsum.photos/400/200" />
`;

export default function Screen() {
  return (
    <RenderHTML
      html={html}
      renderImage={(props) => <Image {...props} />}
      baseStyle={{ fontSize: 16, lineHeight: 24, color: '#111' }}
      onLinkPress={({ url }) => Linking.openURL(url)}
    />
  );
}
```

---

## Props

### `html` (required)

```ts
html: string
```

The HTML string to parse and render.

---

### `renderImage`

```ts
renderImage?: (props: ImageProps) => ReactNode
```

Render prop invoked for every image in the output: `<img>` tags and unordered-list bullet markers. Receives standard React Native `ImageProps` (`source`, `style`, `alt`, …).

When omitted the built-in React Native `<Image>` component is used.

```tsx
// Using expo-image for better caching
import { Image } from 'expo-image';

<RenderHTML
  html={html}
  renderImage={(props) => <Image {...props} contentFit="contain" />}
/>
```

---

### `renderTextComponent`

```ts
renderTextComponent?: (props: TextProps) => ReactNode
```

Render prop invoked for every text node. Receives standard React Native `TextProps`. Use this to swap in a custom typography component.

```tsx
<RenderHTML
  html={html}
  renderTextComponent={(props) => <MyAppText {...props} />}
/>
```

---

### `baseStyle`

```ts
baseStyle?: TextStyle
```

Base text style applied to all elements before per-tag overrides. Commonly used to set a global font family, size, line height, or colour.

---

### `tagStyles`

```ts
tagStyles?: Partial<TagStyles>
```

Per-tag style overrides. Each key is an HTML tag name; the value is `{ text?: TextStyle; block?: ViewStyle }`.

```tsx
<RenderHTML
  html={html}
  tagStyles={{
    h1: { text: { color: 'navy' } },
    blockquote: { block: { borderLeftColor: '#6366f1' } },
    code: { text: { fontFamily: 'JetBrainsMono' } },
    section: { block: { marginVertical: 16 } },
  }}
/>
```

---

### `classesStyles`

```ts
classesStyles?: { [className: string]: { text?: TextStyle; block?: ViewStyle } }
```

Style overrides keyed by HTML `class` attribute value. Applied on top of the matching tag style.

```tsx
<RenderHTML
  html='<p class="lead">Introduction paragraph</p>'
  classesStyles={{
    lead: { text: { fontSize: 20, fontWeight: '600' } },
  }}
/>
```

---

### `listGap`

```ts
listGap?: number
```

Vertical gap between items in `<ul>` and `<ol>` lists. Maps to the flex `gap` property. Defaults to `0`.

---

### `onLinkPress`

```ts
onLinkPress?: (options: { url: string; title: string }) => void
```

Called when the user taps an `<a>` element.

```tsx
import { Linking } from 'react-native';

<RenderHTML
  html={html}
  onLinkPress={({ url }) => Linking.openURL(url)}
/>
```

---

### `markerColor`

```ts
markerColor?: string
```

Colour of bullet markers in unordered lists. Falls back to the inherited text colour, then `'pink'`.

---

### `renderLinkIcon`

```ts
renderLinkIcon?: (href: string, style: ImageStyle) => ReactNode
```

Render prop called for every `<a>` element. Receives the raw `href` string and a pre-computed `ImageStyle` sized and tinted to match the surrounding text (`height`, `width`, `tintColor`, `paddingTop`).

Return a node to display an icon after the link text, or `null`/`undefined` to show nothing. When this prop is omitted no icon is rendered.

```tsx
import { Image } from 'expo-image';

<RenderHTML
  html={html}
  renderLinkIcon={(href, style) => {
    const isExternal = href.startsWith('http://') || href.startsWith('https://');
    if (!isExternal) return null;
    return <Image source={{ uri: MY_EXTERNAL_ICON_URI }} style={style} contentFit="contain" />;
  }}
/>
```

---

## Supported HTML tags

| Category | Tags |
|---|---|
| Headings | `h1` `h2` `h3` `h4` `h5` `h6` |
| Sectioning | `main` `section` `article` `aside` `nav` `header` `footer` |
| Text block | `p` `blockquote` `pre` `div` |
| Inline | `a` `b` `strong` `i` `em` `u` `s` `del` `mark` `small` `sup` `sub` `span` `code` |
| Lists | `ul` `ol` `li` `dl` `dt` `dd` |
| Media | `img` |
| Table | `table` `thead` `tbody` `tfoot` `tr` `th` `td` |
| Other | `hr` `br` |

Unknown tags (e.g. `script`, `style`, `iframe`) are silently ignored.

---

## Default styles

The renderer ships with sensible defaults for all supported tags. Every default can be overridden via `tagStyles`.

| Tag | Default |
|---|---|
| `h1` | 32px, line-height 38.4, top/bottom margin |
| `h2` | 24px, line-height 28.8, top/bottom margin |
| `h3` | 18.72px, matching margin |
| `h4–h6` | Scaled down accordingly |
| `p` | `marginTop: 8` / `marginBottom: 8` (collapsed at container edges) |
| `a` | Underlined; optional icon via `renderLinkIcon` |
| `blockquote` | Italic, 4px left border, grey background |
| `pre` / `code` | Monospace font, grey background |
| `ul` / `ol` | `paddingLeft: 12`, configurable gap |
| `b` / `strong` | Bold |
| `i` / `em` | Italic |
| `u` | Underline |
| `s` / `del` | Strikethrough |
| `mark` | `backgroundColor: '#fff59d'` |
| `small` | `max(baseSize − 2, 12)` |
| `sup` / `sub` | `max(baseSize − 4, 10)`, aligned top/bottom |
| `hr` | 1px bottom border, `marginVertical: 12` |
| `img` | `flex: 1`, `height: 100`, `marginVertical: 8`; aspect ratio corrected after async `Image.getSize` |
| `th` | Bold, `padding: 8`, 1px border |
| `td` | `padding: 8` |
| `section` / `article` | `marginVertical: 8` |
| `header` | `marginBottom: 8` |
| `footer` | `marginTop: 8` |
| `main` / `aside` / `nav` | No default margin (semantic wrappers) |

---

## Known issues

The following elements are partially supported but do not yet render correctly in all cases:

| Tag | Issue |
|---|---|
| `sub` | Vertical offset relies on `textAlignVertical`, which is Android-only. No visual offset on iOS. |
| `sup` | Same as `sub` — no visual offset on iOS. |
| `code` | Rendered as an inline `<Text>` span. When used outside a `<pre>` block, padding and background may bleed into surrounding text or be clipped by the parent `<Text>`. |
| `pre` | Newlines and whitespace are not preserved. The HTML source has all newlines stripped before parsing, and all remaining whitespace is collapsed during text-node rendering. Code blocks appear as a single unwrapped line. |

---

## License

Apache 2.0 — see [LICENSE](./LICENSE) for details.