react-refractor
A thin React wrapper around refractor/Prism that renders syntax-highlighted code as virtual DOM instead of touching the DOM directly.
Repository Health
Technical Analysis
react-refractor is a super-thin React wrapper for refractor, the AST-based syntax highlighter built on Prism. Instead of having Prism manipulate the DOM directly, react-refractor walks the highlighting AST produced by refractor and converts it into React elements, letting React’s own virtual DOM diffing handle updates efficiently. This makes it possible to reuse the exact same highlighting output on both the server and the client, and to only re-render the parts of a code block that actually changed.
The library ships as an ESM-only package for React 18+ and stays deliberately minimal: no bundled languages by default (you register only the Prism/refractor language grammars you need, keeping bundle size down), no automatic stylesheet injection (you bring your own Prism theme), and no dependency on Prism’s DOM-centric plugin system. In exchange for giving up Prism plugins, it gains a custom “markers” feature for highlighting specific lines (or ranges of lines) with your own class names or fully custom React components, plus first-class support for inline rendering and plain-text passthrough.
What You Get
- A
Refractorcomponent that takesvalueandlanguageprops and renders highlighted<pre><code>output - A
registerLanguage/hasLanguageAPI for opting in to only the Prism/refractor grammars you actually use, keeping bundle size small - A
markersprop for highlighting specific lines with a custom class name or a fully custom wrapper component inlineandplainTextrendering modes for inline code snippets and unhighlighted passthrough- Full TypeScript types for props, markers, and the underlying AST node shapes
Common Use Cases
- Rendering highlighted code blocks in documentation sites and blog platforms built on React
- Server-side rendering of syntax-highlighted code (e.g. in Next.js or other SSR frameworks) without a DOM
- Highlighting specific lines of a snippet to draw attention to a diff, error, or change in a tutorial
- Embedding inline highlighted code fragments within prose, not just full code blocks
- Building CMS or documentation tooling (e.g. Sanity Studio, which the maintainer also builds) that needs code-block previews
Under The Hood
Architecture
The package is a thin adapter layer over refractor’s highlight() output (in src/Refractor.tsx): it calls refractor/core’s highlight(value, language) to get a hast AST, optionally runs it through addMarkers (src/addMarkers.ts) to annotate and re-wrap lines for the markers feature, then walks the resulting tree recursively in mapChildren.ts via createElement calls keyed by depth and index, producing a tree of React elements instead of DOM nodes. The marker-wrapping logic in addMarkers.ts is the most involved piece: it assigns line-start/line-end metadata to every AST node via a custom lineNumberify pass, then for each requested marker line it “unwraps” the tree along shared ancestor paths (using unist-util-visit-parents and unist-util-filter) to isolate and rewrap just the nodes on that line, preserving nesting elsewhere. Swapping out the core highlighting engine (refractor/Prism) would mean rewriting most of this file, since the marker logic depends heavily on hast’s AST shape.
Tech Stack
Written in TypeScript, exporting ESM only, and built with @sanity/pkg-utils. It depends on refractor (Prism-based AST highlighter), unist-util-filter, and unist-util-visit-parents for AST traversal — all part of the unified/hast ecosystem rather than anything React-specific. react is a peer dependency (>=18) so consumers control their own React version. The demo app and build tooling use Vite; tests run under Vitest.
Code Quality
Tests live under test/ and use Vitest with snapshot testing (__snapshots__/), covering both Refractor rendering (via react-dom/server) and the addMarkers line-wrapping logic against a fixed AST fixture. Type safety is thorough — the library exports its own prop and marker types, and internal functions use type guards (e.g. isReactRefractorMarkerDataWithComponent) rather than casts. ESLint is configured via the sanity shareable config plus prettier for formatting, and posttest runs lint as part of npm test, so CI-style checks are wired into the standard test command even without a separate CI config visible in the shallow clone.
What Makes It Unique Most React syntax-highlighting wrappers either re-run Prism against the live DOM (losing SSR compatibility and fighting React’s reconciliation) or ship their own bespoke highlighting grammars. react-refractor instead treats highlighting as a pure AST transform — reusing refractor’s Prism-grammar output as data, not DOM — which is what makes identical server/client rendering and React-native diffing possible, and is also what enables the line-marker feature to operate as a structural AST rewrite rather than post-hoc DOM manipulation.