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

@k11k/better-blocks-react-renderer

v0.10.1

Published

React renderer for Strapi v5 Blocks content with full Better Blocks plugin support — colors, tables, to-do lists, media embeds, alignment, and more

Readme


Table of Contents

  1. Why?
  2. Compatibility
  3. Installation
  4. Usage
  5. Supported Blocks
  6. Supported Modifiers
  7. Custom Renderers
  8. TypeScript
  9. Contributing
  10. Support this project
  11. License

Why?

The official @strapi/blocks-react-renderer doesn't support the features that the Better Blocks plugin adds to the Strapi editor — color marks, text alignment, to-do lists, tables, media embeds, and more.

This package is a drop-in replacement that renders all Better Blocks features out of the box — no configuration needed.

Compatibility

| Strapi Version | Renderer Version | React Version | | -------------- | ---------------- | ------------- | | v5.x | v0.x | ≥ 17 |

Installation

# Using yarn
yarn add @k11k/better-blocks-react-renderer

# Using npm
npm install @k11k/better-blocks-react-renderer

Peer dependencies: react >= 17

Usage

import { BlocksRenderer } from '@k11k/better-blocks-react-renderer';

// Basic — renders all blocks including color/highlight
<BlocksRenderer content={blocks} />;

That's it. All Better Blocks features — colors, tables, to-do lists, media embeds, alignment, and more — work automatically.

Math (KaTeX)

Math nodes are rendered with KaTeX — inline math becomes a <span class="katex-inline"> and block math a <div class="katex-block">. Rendering happens via katex.renderToString, so it works in SSR and during static export with no client-side hydration step.

KaTeX needs its stylesheet to display correctly. Import it once in your app entry point:

import 'katex/dist/katex.min.css';

katex ships as a dependency of this package, so the stylesheet resolves without a separate install. If KaTeX fails to parse a formula, the renderer falls back to the raw LaTeX source instead of crashing.

Diagrams (Mermaid)

Block-level diagram nodes (format: 'mermaid') are rendered to inline SVG with Mermaid — flowcharts, sequence, class, state, ER, pie charts, and more.

Unlike KaTeX, Mermaid needs a real browser DOM to measure text, so it cannot render synchronously on the server. The renderer keeps SSR/static export safe by emitting the raw Mermaid source inside a <pre class="mermaid-source"> on the server and during the first client render (so hydration matches), then swapping in the rendered <div class="mermaid-diagram"> SVG after mount. If Mermaid fails to parse the source, the raw text stays in place as a graceful fallback.

mermaid ships as a dependency and is lazy-loaded the first time a diagram renders, so it stays out of your server bundle and only downloads on pages that actually use a diagram — no setup or stylesheet import required.

To render diagrams yourself (e.g. a different engine or custom theming), override the diagram block:

<BlocksRenderer
  content={blocks}
  blocks={{
    diagram: ({ code, format }) => <MyDiagram code={code} format={format} />,
  }}
/>

Callouts (Admonitions)

Block-level callout nodes render GitHub-style alerts in five variants — note, tip, important, warning, and caution. Each renders as an <aside role="note"> with a colored left border, a title row (icon + label), and the nested block children (paragraphs, lists, links, etc.). If a title is set on the node it is used; otherwise the localized variant label is shown. Colors are applied inline, so there is no stylesheet to import.

To match your design system, override the callout block. It receives variant, title, and the already-rendered children:

<BlocksRenderer
  content={blocks}
  blocks={{
    callout: ({ variant, title, children }) => (
      <div className={`alert alert-${variant}`}>
        {title && <h4>{title}</h4>}
        {children}
      </div>
    ),
  }}
/>

Styling & dark mode. The default markup carries stable classes — bb-callout, bb-callout-{variant}, bb-callout-title, and bb-callout-icon — which you can target for spacing, typography, radius, etc. The accent colors are applied inline (so the default works with zero setup), which means you can't recolor them with a plain CSS class. To re-theme colors — including a dark-mode palette — override the callout block and apply your own colors per variant:

const ACCENT: Record<string, string> = {
  note: 'var(--cl-note, #4493f8)',
  tip: 'var(--cl-tip, #3fb950)',
  important: 'var(--cl-important, #ab7df8)',
  warning: 'var(--cl-warning, #d29922)',
  caution: 'var(--cl-caution, #f85149)',
};

<BlocksRenderer
  content={blocks}
  blocks={{
    callout: ({ variant, title, children }) => (
      <aside
        className={`callout callout-${variant}`}
        style={{ borderLeft: `4px solid ${ACCENT[variant]}` }}
      >
        <p style={{ color: ACCENT[variant], fontWeight: 600 }}>{title ?? variant}</p>
        {children}
      </aside>
    ),
  }}
/>;

Driving the accent from CSS variables (as above) lets you flip palettes with a @media (prefers-color-scheme: dark) or a .dark class rule on a parent.

Details / Summary (Collapsible)

Block-level details nodes render a native, keyboard-accessible <details> / <summary> disclosure. The summary field is the plain-text label, the optional defaultOpen boolean maps to the HTML open attribute (honored on initial render so screen readers get the correct state), and children are block-level content (paragraphs, lists, tables, images, and nested details) rendered after the summary. The default markup carries stable bb-details and bb-details-summary classes for styling.

To match your design system, override the details block. It receives summary, defaultOpen, and the already-rendered children:

<BlocksRenderer
  content={blocks}
  blocks={{
    details: ({ summary, defaultOpen, children }) => (
      <details open={defaultOpen} className="custom-details">
        <summary>{summary}</summary>
        {children}
      </details>
    ),
  }}
/>

Buttons (CTA & File Download)

Block-level button nodes render a WordPress-style call-to-action. The buttonType selects the mode:

  • link — renders <a href={link.url} target={link.target} rel={link.rel} aria-label={link.ariaLabel}>{label}</a>.
  • file — renders a download link <a href={file.url} download={file.name} aria-label="Download …">, optionally prefixed with a file-type icon (showFileIcon) and suffixed with a human-readable size (showFileSize). Clicking force-downloads the file via a blob fetch, which works even when the asset is hosted cross-origin (where the native download attribute is otherwise ignored and the browser previews PDFs/videos/images inline). If the fetch is CORS-blocked, it falls back to native navigation. Set filePreview: true to instead open the file in a new tab (target="_blank" rel="noopener noreferrer", no download) so users can preview it before saving.

The style object is applied as inline CSS (backgroundColor, colortextColor, borderRadius, fontSize, fontWeight, padding, border). The block is wrapped in a <div className="bb-button-wrapper"> whose text-align honors alignment (left / center / right); alignment: "none" renders the button inline with no wrapper. A cssClass is appended to the default bb-button class.

Hover colors. hoverBackgroundColor / hoverTextColor work out of the box — no setup, no stylesheet import. The renderer ships a small <style> (emitted once, only when a default button is present) that wires the hover and :focus-visible states to the --bb-button-hover-bg / --bb-button-hover-color custom properties it sets from those fields. Buttons without hover colors keep their base colors on hover.

To customize the hover behavior, target .bb-button:hover yourself. Because the base colors are applied inline, your rule needs !important to win:

.bb-button:hover {
  background-color: #3732c9 !important;
  color: #fff !important;
}

To fully control the markup, override the button block. It receives label, buttonType, alignment, link, file, showFileSize, showFileIcon, filePreview, style, and cssClass:

<BlocksRenderer
  content={blocks}
  blocks={{
    button: ({ label, link, alignment }) => (
      <div className={`button-wrapper align-${alignment}`}>
        <a href={link?.url} target={link?.target} rel={link?.rel}>
          {label}
        </a>
      </div>
    ),
  }}
/>

Astro

BlocksRenderer works in Astro via the @astrojs/react integration. Because the renderer is purely presentational and KaTeX renders to a string on the server (see Math (KaTeX)), you can render it as a static Astro island with no client directive — Astro outputs plain HTML and ships zero JavaScript:

---
import { BlocksRenderer } from '@k11k/better-blocks-react-renderer';
// Import the KaTeX stylesheet once (e.g. in a shared layout) so math displays correctly.
import 'katex/dist/katex.min.css';

const { blocks } = Astro.props;
---

<BlocksRenderer content={blocks} />

You only need a client directive (client:load, client:visible, etc.) if you pass interactive custom renderers — for example a to-do list-item with a working checkbox, or a custom math renderer that hydrates on the client. Static content (including server-rendered KaTeX) needs no hydration:

---
import { BlocksRenderer } from '@k11k/better-blocks-react-renderer';
const { blocks } = Astro.props;
---

<!-- Use a client directive only when your custom renderers need to run in the browser -->
<BlocksRenderer content={blocks} client:visible />

Note: When you hydrate with a client directive, custom renderers passed as props must be serializable references (e.g. imported components), since Astro serializes island props. Keep inline closures for the static (no-directive) case.

Supported Blocks

| Block | Default element | Source | | ------------------------------- | ------------------- | --------------------------- | | paragraph | <p> | Strapi core | | heading (1–6) | <h1><h6> | Strapi core | | list (ordered/unordered/todo) | <ol> / <ul> | Strapi core + Better Blocks | | list-item | <li> | Strapi core | | link | <a> | Strapi core | | quote | <blockquote> | Strapi core | | code | <pre><code> | Strapi core | | image | <figure><img> | Strapi core | | horizontal-line | <hr> | Better Blocks | | table | <table> | Better Blocks | | media-embed | <iframe> (16:9) | Better Blocks | | math (inline/block) | <span> / <div> | Better Blocks | | diagram (mermaid) | <div> (SVG) | Better Blocks | | callout (admonition) | <aside> | Better Blocks | | details (collapsible) | <details> | Better Blocks | | button (CTA / file download) | <a> | Better Blocks |

Block properties

| Property | Applies to | Description | | -------------- | ------------------------- | --------------------------------------------------------- | | textAlign | paragraph, heading, quote | Text alignment (left, center, right, justify) | | lineHeight | paragraph, heading, quote | CSS line-height value (e.g. 1.5, 2.0) | | indent | paragraph, heading, quote | Block indentation level (marginLeft: N * 2rem) | | indentLevel | list | Cycling list-style-type per nesting depth | | format | list | ordered, unordered, or todo | | checked | list-item (in todo lists) | Checkbox state (true/false) | | target | link | _blank for new-tab links | | rel | link | noopener noreferrer for new-tab links | | caption | image | Text displayed below the image | | imageAlign | image | Image alignment (left, center, right) | | url | media-embed | Embed URL (YouTube/Vimeo iframe src) | | originalUrl | media-embed | Original user-provided URL | | format | math | inline (<span>) or block (<div>) | | value | math | LaTeX source rendered with KaTeX | | format | diagram | mermaid | | value | diagram | Mermaid source rendered to SVG | | summary | details | Plain-text label for the <summary> | | defaultOpen | details | Open on initial render (HTML open attribute) | | buttonType | button | link or file (download) mode | | label | button | Visible button text | | alignment | button | left, center, right, or none (inline) | | link | button (link mode) | { url, target, rel, ariaLabel } | | file | button (file mode) | { url, name, size, ext, mime } for download | | showFileIcon | button (file mode) | Prefix the label with a file-type icon | | showFileSize | button (file mode) | Suffix the label with a human-readable size | | filePreview | button (file mode) | true opens the file in a new tab instead of downloading | | style | button | Inline CSS + hover custom properties | | cssClass | button | Extra class appended to bb-button |

Supported Modifiers

| Modifier | Default element | Source | | ----------------- | ---------------------------------- | ------------- | | bold | <strong> | Strapi core | | italic | <em> | Strapi core | | underline | <span> | Strapi core | | strikethrough | <del> | Strapi core | | code | <code> | Strapi core | | uppercase | <span style={{textTransform}}> | Better Blocks | | superscript | <sup> | Better Blocks | | subscript | <sub> | Better Blocks | | color | <span style={{color}}> | Better Blocks | | backgroundColor | <span style={{backgroundColor}}> | Better Blocks | | fontFamily | <span style={{fontFamily}}> | Better Blocks | | fontSize | <span style={{fontSize}}> | Better Blocks |

Custom Renderers

Custom block renderers

Override any block type with your own component:

<BlocksRenderer
  content={blocks}
  blocks={{
    paragraph: ({ children, style }) => (
      <p className="my-paragraph" style={style}>
        {children}
      </p>
    ),
    heading: ({ children, level, style }) => {
      const Tag = `h${level}`;
      return <Tag style={style}>{children}</Tag>;
    },
    link: ({ children, url, target, rel }) => (
      <a href={url} target={target} rel={rel}>
        {children}
      </a>
    ),
    image: ({ image, caption, imageAlign }) => (
      <figure style={{ textAlign: imageAlign }}>
        <img src={image.url} alt={image.alternativeText || ''} loading="lazy" />
        {caption && <figcaption>{caption}</figcaption>}
      </figure>
    ),
    'list-item': ({ children, checked }) =>
      checked !== undefined ? (
        <li style={{ listStyle: 'none' }}>
          <input type="checkbox" checked={checked} readOnly /> {children}
        </li>
      ) : (
        <li>{children}</li>
      ),
    'horizontal-line': () => <hr className="my-divider" />,
    table: ({ children }) => <table className="my-table">{children}</table>,
    'table-header-cell': ({ children }) => <th className="my-th">{children}</th>,
    'table-cell': ({ children }) => <td className="my-td">{children}</td>,
    'media-embed': ({ url }) => (
      <div className="video-wrapper">
        <iframe src={url} allowFullScreen title="Embedded media" />
      </div>
    ),
    // Bring your own math engine (e.g. MathJax) instead of the built-in KaTeX
    math: ({ formula, inline }) =>
      inline ? <MyInlineMath formula={formula} /> : <MyBlockMath formula={formula} />,
    // Bring your own diagram engine instead of the built-in Mermaid
    diagram: ({ code, format }) => <MyDiagram code={code} format={format} />,
  }}
/>

Custom modifier renderers

Override any text modifier with your own component:

<BlocksRenderer
  content={blocks}
  modifiers={{
    bold: ({ children }) => <strong className="font-bold">{children}</strong>,
    color: ({ children, color }) => <span style={{ color }}>{children}</span>,
    backgroundColor: ({ children, backgroundColor }) => (
      <mark style={{ backgroundColor }}>{children}</mark>
    ),
  }}
/>

TypeScript

All types are exported:

import type {
  BlocksContent,
  BlocksRendererProps,
  BlockNode,
  TextNode,
  LinkNode,
  ListNode,
  ListItemNode,
  ParagraphNode,
  HeadingNode,
  QuoteNode,
  CodeNode,
  ImageNode,
  HorizontalLineNode,
  TableNode,
  TableRowNode,
  TableCellNode,
  TableHeaderCellNode,
  MediaEmbedNode,
  MathNode,
  DiagramNode,
  TextAlign,
  CustomBlocksConfig,
  CustomModifiersConfig,
} from '@k11k/better-blocks-react-renderer';

Contributing

Contributions are welcome! The easiest way to get started is with Docker:

# Clone the repository
git clone https://github.com/k11k-labs/better-blocks-react-renderer.git
cd better-blocks-react-renderer

# Start the playground with Docker
cd playground
docker compose up

This will start a Strapi v5 instance with the Better Blocks plugin and a React app that renders the content — all pre-configured with a showcase article.

  • Strapi admin: http://localhost:1337/admin (login: [email protected] / admin12#)
  • React app: http://localhost:5173

Development workflow

  1. Make changes to the renderer source in src/
  2. Rebuild: yarn build (from repo root)
  3. The React app picks up the new build automatically

Without Docker

# Build the renderer
yarn install && yarn build

# Start Strapi
cd playground/strapi && cp .env.example .env && npm install && npm run dev

# Start the React app (in another terminal)
cd playground/react-app && npm install && npm run dev

Running tests

yarn test        # Run tests
yarn test:ts     # Type check
yarn lint        # Check formatting

Community & Support

Support this project

This package is built and maintained in my free time, and it's free for everyone. If it has saved you time on a project, you can help keep it caffeinated and actively developed:

Every coffee goes toward fixing bugs, reviewing PRs, writing docs, and shipping the features you ask for. Thank you! ☕

Related

License

MIT License © k11k-labs