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

@mdxstudio/react

v0.3.0

Published

MdxRenderer and the built-in MDX components: callouts, cards, stats, tabs, steps and the rest.

Readme

@mdxstudio/react

Renders an MDX string to React. MdxRenderer parses the document, evaluates its expressions and mounts the result with a set of built-in components — callouts, cards, stat grids, tabs, accordions, side-by-side splits, steps, timelines, badges and syntax-highlighted code blocks — none of which the consuming app has to write.

Install

npm install @mdxstudio/react @mdxstudio/core react

@mdxstudio/core and react are peer dependencies. npm 7+ installs peers for you; pnpm and yarn need them listed explicitly.

| Peer | Range | Why | | --------------- | ---------- | ------------------------------------------------------------ | | react | ^19.0.0 | One React instance, or hooks throw. | | @mdxstudio/core | ^0.1.0 | Holds the render context every mdxstudio package reads from — a second copy silently breaks theming inside diagrams and charts. |

Usage

import { MdxRenderer, THEMES } from '@mdxstudio/react';
import '@mdxstudio/react/styles.css';

const source = `---
title: Release notes
author: Ada
---

# Hello

<Callout type="info" title="Heads up">
  Components are available without importing them.
</Callout>

<StatGrid cols={2}>
  <Stat title="Uptime" value="99.98%" change="+0.02%" trend="up" />
  <Stat title="p95 latency" value="120ms" trend="neutral" />
</StatGrid>

\`\`\`ts
export const answer = 42;
\`\`\`
`;

export function Doc() {
  return <MdxRenderer content={source} themeConfig={THEMES['github-light']} />;
}

themeConfig is required. THEMES is keyed by ThemeId: frosted-glass, github-light, github-dark, dracula, nord, editorial, cyberpunk, forest.

Stylesheet

import '@mdxstudio/react/styles.css';

Plain CSS — no Tailwind, no preprocessor, no build step. Import it once, anywhere in your app. The theme is applied through CSS custom properties that MdxRenderer sets from themeConfig, so one stylesheet covers every theme.

Adding components

MdxRenderer only knows its own components by default. Diagrams, charts and flow graphs ship separately so their weight is opt-in:

// mdxRegistry.ts — module scope, so the object identity is stable.
import { createRendererRegistry } from '@mdxstudio/react';
import { mermaidPlugin } from '@mdxstudio/mermaid';
import { chartsPlugin } from '@mdxstudio/charts';

export const registry = createRendererRegistry(mermaidPlugin, chartsPlugin);
<MdxRenderer content={source} themeConfig={THEMES['nord']} registry={registry} />

Passing a component under a built-in name replaces it. Build the registry once at module scope: a new registry object on every render re-parses the document.

That is how your application gets your components. A reader who is not running your application — someone browsing the repository with npx @mdxstudio/cli serve ./docs, or previewing a file in VS Code with the MDX Studio Preview extension — reaches the same components through an mdxstudio.config.js in the folder, whose default export those two hosts turn into a registry source and apply after their own built-ins:

// mdxstudio.config.js
export default ({ createElement }) => ({
  components: { Ticket: ({ id }) => createElement('code', null, `#${id}`) },
  aliases: { Issue: 'Ticket' },
});

It runs in the browser, so createElement rather than JSX and no bare imports. The extension will not load one in a workspace you have not trusted.

Math

$inline$ and $$block$$ are typeset with KaTeX. Nothing to register and nothing to import: the parser turns math into <MathExpression>, which loads KaTeX and its stylesheet on demand, so a document with no equations downloads neither. Until the chunk arrives the TeX source is what is on the page.

katex is a dependency of this package; if your bundler needs help with the katex/dist/katex.min.css import inside the lazy chunk, alias it — the CLI does exactly that in packages/cli/scripts/build-client.mjs.

Alerts

GitHub's alert blockquotes render through the built-in Callout, so they are themed by the same --mdxstudio-callout-* properties:

> [!WARNING]
> Mind the gap.

Headings

h1 to h3 each open a section a reader can collapse: everything under the heading, down to the next heading of the same level or a shallower one, folds away behind a chevron in the heading itself. h4 to h6 get no chevron of their own — they label a paragraph rather than start a chapter — and fold away with the section they sit in. Everything starts open, and a heading with nothing under it gets no chevron at all.

The chevron is the only thing that opens and closes a section, so selecting a sentence out of a heading never folds the text under it away. It is a real button: aria-expanded, aria-controls, Enter and Space. A collapsed section's content is unmounted rather than hidden, so a heading inside it stops answering document.getElementById — which is what stops a table of contents or an editor scroll sync from measuring a heading that is not on the page.

Beside it is a copy control, which puts the heading's fragment on the clipboard — #the-heading — and nothing else. That is the one form that means the same thing in every host: pasted into a document it is a working in-page link, and given a path in front of it, the cross-document link the VS Code extension resolves (./other.mdx#the-heading). A full URL would be a vscode-webview:// address or a port on somebody's laptop, neither of which survives being shared. The ids are the ones extractHeadings() reports, so a copied link and a table of contents entry cannot disagree.

The heading itself is untouched: a real h1, h2 or h3, with its id, in document order, and with only its own text in it. In renderMode="pdf" every section is open and neither control is rendered, so nothing is lost to the exporter.

Accordions

Panels are children, so anything a document can hold goes inside one — lists, fenced code, other components, a diagram:

<Accordion>

<AccordionItem title="How does parsing work?" icon="BookOpen" badge="New">

Full **markdown** here, because the panel goes through the MDX pipeline.

- a list
- another item

</AccordionItem>

<AccordionItem title="What about a diagram?" subtitle="Yes, that too">
  Anything at all.
</AccordionItem>

</Accordion>

Markdown inside a panel needs a blank line after the opening tag and before the closing one — that is MDX, not this component. Panels written compactly on one line each still group correctly; only the markdown in them stays literal.

AccordionItem takes title plus the same optional icon (a lucide name), subtitle and badge that Card does, and defaultOpen.

The accordion itself takes multiple, which lets more than one panel be open at once, and defaultOpen, which decides what is open on load: an index, a title, "all", "none", or a list of those. Given neither, the panels marked defaultOpen open, and if none are, the first one does — an accordion arrives with something to read. <Accordion defaultOpen="none"> starts shut.

The trigger is a real button: aria-expanded, aria-controls, Enter and Space to open and close, Up/Down/Home/End to move between panels. A closed panel stays mounted and hidden, so a component inside it keeps its state.

In renderMode="pdf" every panel is open and the trigger is not a button, so nothing is lost to the exporter — which deletes every button and photographs only what is visible.

The 0.2.3 prop form still renders, and reads the same extra fields:

<Accordion items={[{ title: 'Question?', content: 'Answer.' }]} />

content is a prop, so markdown written there stays literal. Children win when a document gives both.

Splits

Two things on the page at once — the one thing Tabs cannot do, because a tab shows one variant at a time and a comparison needs both together. Pane content is children, so anything a document can hold goes in one: fences, lists, other components, a diagram.

<Split ratio="60/40">

<Pane title="Before" icon="Ban">

```ts
const x = 1;
```

</Pane>

<Pane title="After" icon="Check" badge="Typed">

```ts
const x: number = 1;
```

</Pane>

</Split>

direction says how the panes are arranged: row (the default) puts them beside each other, column stacks them. It deliberately does not take horizontal or vertical — those name the divider to some readers and the arrangement to others. The divider's own axis is the opposite one, and is reported where it matters: aria-orientation is vertical for a row of panes and horizontal for a column of them.

ratio sets where the split starts. A list of weights spelled any of the usual ways — "60/40", "2:1", "3 1", {[3, 1]} — or a single number, which is the first pane's percentage. Anything unreadable gives equal panes, and no pane is ever narrower than 10%. More than two panes work; each divider moves only the two panes it sits between.

Dragging a divider changes the split for the session only — nothing is stored, and a reload is back to what the document said. The divider is focusable: arrow keys move it by 2%, Shift by 10%, Home (or a double-click) puts the authored ratio back.

height gives the split a fixed size, and its panes scroll instead of growing. A column split needs one for its divider to have anything to move, so it takes 24rem unless told otherwise; a row split grows with its content by default.

Pane takes title, plus the same optional icon (a lucide name) and badge that Card does. Compare is another name for Split.

Below 48rem a row of panes stacks, because two fences side by side in a narrow column are unreadable, and the divider goes with it — there is nothing left for it to move.

In renderMode="pdf" the layout is decided against the A4 sheet rather than against the reader's window. Two panes print side by side down to 60/40; a three-way split, or anything more lopsided, is stacked instead, each pane at full width under its own title — and a pane that never named itself is numbered, so a stacked export is never a column of unlabelled content. The divider is still drawn but is not a button, so the exporter has nothing to delete.

Images

Clicking an image opens it enlarged over the document; Escape, the close button or a click outside closes it, and focus returns to the image, which is itself focusable and answers Enter and Space. Turn it off with lightbox={false}; it is off automatically in renderMode="pdf".

Untrusted documents

expressions="literals" restricts {...} to values the syntax spells out. For a document you did not write, that is not enough on its own — the components themselves still run in your page. Use @mdxstudio/sandbox.

Exports

MdxRenderer, FrontmatterHeader, InlineToken, MdxImage, ImageLightbox, THEMES, reactPlugin, createRendererRegistry, baseMdxRegistry, and every built-in component (Callout, Card, CardGrid, Stat, StatGrid, Tabs, Tab, Accordion, AccordionItem, Split, Pane, Steps, Step, Timeline, ProgressBar, InteractiveCounter, Kbd, Badge, Button, TableComponent, InlineCode, MathExpression).

ESM only, with TypeScript declarations.

License

MIT