Skip to content
Merged
64 changes: 64 additions & 0 deletions docs/blog/2026-07-27-state-of-react/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
slug: state-of-react-2026-07
title: 'The State of React — July 2026'
description: 'A dated, linkable snapshot comparing bestax against Mantine, MUI, Chakra UI, shadcn/ui, React-Bootstrap, and react-bulma-components — capability by capability, with every cell linking to the real docs.'
authors: [asmith]
tags: [state-of-react, comparison, react, bulma, mantine, mui, chakra, shadcn]
canonical_url: https://bestax.io/blog/state-of-react-2026-07
publish_to_devto: false
hide_table_of_contents: true
image: /img/state-of-react/2026-07.png
cover_image: /img/state-of-react/2026-07.png
---

import ComponentComparison from '@site/src/components/ComponentComparison';

<img
className="sor-cover"
src="/img/state-of-react/2026-07.svg"
alt="The State of React — July 2026: React component libraries, compared"
/>

Picking a React component library is one of the bigger architectural calls in a project, and the honest answer to “which one” depends entirely on what you’re building. We maintain [bestax](/docs/intro), a component library for Bulma v1, so we spend a lot of time studying the field. This post maps it out **fairly and in the open**: every user-facing capability across seven React libraries, with each cell linking straight to that library’s own documentation so you can check our work.

It’s a point-in-time capture — the first edition of a series we’ll refresh regularly. We’re calling it _The State of React_, with a nod to Armin’s long-running radio show. Over time we’d like it to grow into a wider read on the React ecosystem; for now, it stays focused on **component libraries**.

:::info Snapshot — July 2026
The table below reflects each library **as of July 27, 2026**. Component catalogs move fast — the [latest edition of _The State of React_](/blog/tags/state-of-react) always has the freshest data.
:::

In the ring: **[bestax](/docs/intro)** (Bulma v1), **[react-bulma-components](https://react-bulma.dev/)** (the other Bulma wrapper), **[Mantine](https://mantine.dev/)**, **[MUI](https://mui.com/)**, **[Chakra UI](https://chakra-ui.com/)**, **[shadcn/ui](https://ui.shadcn.com/)**, and **[React-Bootstrap](https://react-bootstrap.netlify.app/)**.

<!-- truncate -->

Each row is a capability; read across to see who ships it. **✓** is a dedicated component, **◐** is available via a prop or by composition, and a muted dash means no first-party equivalent. The columns are abbreviated (hover a header for the full name); **hover any cell for the component name, or click it** to open that component’s docs.

<ComponentComparison />

## What the matrix says

:::tip bestax is a superset of react-bulma-components
Within the Bulma world there isn’t much contest left. `react-bulma-components` covers Bulma-core only and has been inactive since ~2021. bestax matches essentially all of it (bar Bulma’s `Tile`) and adds roughly 50 more components — date/time pickers, autocomplete, rating, tag input, slider, switch, Carousel, Steps, Avatar, Badge, Toast, Tooltip, Skeleton, Sidebar, Dialog, and Reveal.
:::

A few honest takeaways across the wider field:

- **Mantine** has the widest first-party catalog — rich inputs (color, PIN, JSON), plus Spotlight, ScrollArea, HoverCard, and a full app-shell layout system.
- **MUI** owns heavy data and app-frame surfaces through its X packages: Data Grid, Charts, Tree View, and Date Pickers, plus FAB, Speed Dial, and Transfer List.
- **Chakra UI** is the closest peer to bestax’s prop-driven styling model and is deep on form controls — but ships **no** date pickers, charts, carousel, or data grid.
- **React-Bootstrap** is the smallest set: a faithful Bootstrap wrapper with no date/time pickers, rating, autocomplete, stepper, or tag input.
- **bestax** covers the mainstream set and adds Bulma-idiomatic layout — Hero, Level, Media, Section, Panel — that most libraries leave to composition.

:::note shadcn/ui plays by different rules
shadcn/ui looks broad because it’s a **copy-paste registry**, not an npm dependency: its behavior comes from Radix UI, cmdk, TanStack Table, and react-day-picker. You own the code it drops into your project — powerful, but a different maintenance model than the others here.
:::

## Corrections welcome

We compiled this by hand from each library’s component source and documentation, so it will have edges — a mislabelled cell, a capability we scoped wrong, or a competitor link that’s drifted. It’s meant to be honest, not perfect.

:::caution Spot something off?
Please [open an issue](https://github.com/allxsmith/bestax/issues) — corrections make the next edition better.
:::

See you in the next one.
81 changes: 81 additions & 0 deletions docs/blog/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# blog — posts, and the "The State of React" series

General blog conventions live in `docs/CLAUDE.md` (frontmatter, `authors.yml`, the
`<!-- truncate -->` fold, and dev.to syndication via `plugins/devto-preprocessor.js`). This file
is the **runbook for the recurring component-comparison series** so each edition is turnkey.

## What the series is

**"The State of React"** is a dated, honest snapshot comparing bestax against the other
mainstream React component libraries (Mantine, MUI, Chakra UI, shadcn/ui, React-Bootstrap, and
react-bulma-components). Each edition is a point-in-time capture; we publish a fresh one on a
roughly **monthly** cadence rather than editing an old post.

Three files back every edition:

- `docs/src/data/componentComparison.js` — the matrix (capabilities × libraries), the per-cell
link resolvers, and `lastReviewed`. **This is the source of truth** — the only file that
changes between most editions.
- `docs/src/components/ComponentComparison/` — the React table that renders it (theme-aware,
links every ✓/◐ to that library's docs). Rarely needs changes.
- `docs/blog/{YYYY-MM-DD}-state-of-react/index.md` — the edition post; imports and renders
`<ComponentComparison />`.

## Conventions (keep these stable)

- **Title:** `The State of React — {Month YYYY}` — the month + year are required.
- **Folder / filename:** `docs/blog/{YYYY-MM-DD}-state-of-react/index.md` (folder form).
- **Slug:** `state-of-react-{YYYY-MM}`; set `canonical_url` to `https://bestax.io/blog/{slug}`.
- **Tag:** always include `state-of-react`. Its archive page,
[`/blog/tags/state-of-react`](https://bestax.io/blog/tags/state-of-react), always lists
the newest edition first — it is the canonical "latest edition" pointer that every edition's top
`:::info` admonition links to. Do **not** change this tag.
- `authors: [asmith]`, `publish_to_devto: false` (the interactive table does not port to plain
markdown / dev.to), and `hide_table_of_contents: true` (the wide table needs the room — this
removes the right-hand TOC; the left blog sidebar is collapsed automatically via a scoped
`:has(.sor-comparison)` rule in `src/css/custom.css`, so no per-post action is needed).
- Keep the four admonitions: top `:::info` (snapshot date + latest-edition link), `:::tip`
(headline insight), `:::note` (shadcn is a copy-paste registry, not a dependency), `:::caution`
(corrections → GitHub issues).

## Publishing a new monthly edition

1. **Update the data** in `docs/src/data/componentComparison.js`:
- Adjust the matrix for anything that shipped or moved since last month (new components,
renamed docs, packages promoted out of "lab"/experimental).
- Cell encoding: `"Name"` = dedicated component · `"~Name"` = via prop/composition · `0` = none.
- Fix or extend the per-library link resolvers/override maps as needed. **bestax** cells are
internal `/docs/api/...` links (validated by the build); competitor links are best-effort deep
links with a per-library fallback — improve a fallback → deep link when you confirm a stable URL.
- **Bump `lastReviewed`** to the review date (`YYYY-MM-DD`).
2. **Create the edition post** by copying the previous month's `index.md`, updating the frontmatter
(title, slug, canonical_url, date in the folder name) and the prose/insights. Keep
`import ComponentComparison ...` and `<ComponentComparison />`.
3. **Verify** (see below), then commit with a `docs` type and push. Open a PR to `main` only when
asked.

## Cover image

Each edition ships a synthwave/EDM cover (an homage to the "A State of Trance" radio show):

- **Source SVG:** `docs/static/img/state-of-react/{YYYY-MM}.svg` — for a new edition, copy the
previous month's and update the month text and the `EP.` number.
- **Rasterize** it to `docs/static/img/state-of-react/{YYYY-MM}.png` at **1200×630** — SVG does
not work as an `og:image` / dev.to `cover_image`, so a raster is required. Load the SVG in a
headless browser at 1200×630 and screenshot it (this repo used Playwright + the pre-installed
Chromium at `/opt/pw-browsers`).
- **Frontmatter:** point both `image:` and `cover_image:` at the `.png` (rooted `/img/...` path).
- **Visible banner:** at the very top of the post body, render the SVG full-width:
`<img className="sor-cover" src="/img/state-of-react/{YYYY-MM}.svg" alt="…" />`.

## Verify

- `pnpm exec turbo run build --filter=@allxsmith/bestax-docs` — **must pass**;
`onBrokenLinks: 'throw'` validates every internal bestax link and the tag-archive link.
- `pnpm format:check` (covers `md`/`mdx`) — run `pnpm format` to autofix.
- Spot-check a representative external link per library (these are not build-validated).
- `pnpm docs` and open `/blog/state-of-react-{YYYY-MM}`: confirm the table renders and scrolls
on narrow widths, is legible in light **and** dark, and the admonitions + archive link work.

The blog is excluded from the LLM index (`includeBlog: false`) by design — this series is a dated
snapshot, not canonical reference documentation.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ Bulma 0.9.x. `@allxsmith/bestax-bulma` is actively maintained, targets **Bulma v
variables, dark mode, Grid), and covers the same component surface — so most apps can migrate
mechanically.

:::tip See how they compare
For a capability-by-capability comparison of bestax against react-bulma-components and the other
major React libraries (Mantine, MUI, Chakra, shadcn/ui, React-Bootstrap), see the latest edition of
[**The State of React**](/blog/tags/state-of-react).
:::

The **`bestax-migrate`** codemod automates that mechanical part. It supports
react-bulma-components **v4** (the TypeScript rewrite most surviving apps use).

Expand Down
10 changes: 10 additions & 0 deletions docs/docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ const config = {
},
blog: {
showReadingTime: true,
// Docusaurus globs every markdown file in blog/ as a post. Keep the
// default exclusions and also skip CLAUDE.md (an agent runbook that
// lives in blog/, not a post).
exclude: [
'**/_*.{js,jsx,ts,tsx,md,mdx}',
'**/_*/**',
'**/*.test.{js,jsx,ts,tsx}',
'**/__tests__/**',
'**/CLAUDE.md',
],
feedOptions: {
type: ['rss', 'atom'],
xslt: true,
Expand Down
199 changes: 199 additions & 0 deletions docs/src/components/ComponentComparison/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import React from 'react';
import clsx from 'clsx';
import Link from '@docusaurus/Link';
import {
libs,
categories,
lastReviewed,
parseCell,
} from '@site/src/data/componentComparison';
import styles from './styles.module.css';

const MONTHS = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
];

function formatReviewed(iso) {
const [y, m, d] = iso.split('-').map(Number);
return `${MONTHS[m - 1]} ${d}, ${y}`;
}

const GLYPH = { full: '✓', partial: '◐', none: '–' };
const STATUS_LABEL = {
full: 'dedicated component',
partial: 'via prop or composition',
none: 'no equivalent',
};

function Cell({ lib, value }) {
const { status, name } = parseCell(value);

if (status === 'none') {
return (
<td className={clsx(styles.cell, styles.noneCell)}>
<span className={clsx(styles.glyph, styles.none)} aria-hidden="true">
{GLYPH.none}
</span>
<span className={styles.srOnly}>none</span>
</td>
);
}

const href = lib.resolve(name);
const internal = href.startsWith('/');
const label = `${lib.title || lib.label} ${name} — ${STATUS_LABEL[status]}`;
const inner = (
<span className={clsx(styles.glyph, styles[status])} aria-hidden="true">
{GLYPH[status]}
</span>
);

return (
<td className={styles.cell}>
{internal ? (
<Link
to={href}
className={styles.cellLink}
title={label}
aria-label={label}
>
{inner}
</Link>
) : (
<a
href={href}
className={styles.cellLink}
title={label}
aria-label={label}
target="_blank"
rel="noopener noreferrer"
>
{inner}
</a>
)}
</td>
);
}

export default function ComponentComparison() {
return (
<div className={clsx(styles.root, 'sor-comparison')}>
<div className={styles.meta}>
<span className={styles.asOf}>
Data as of <strong>{formatReviewed(lastReviewed)}</strong>
</span>
<span className={styles.latest}>
A newer edition may exist —{' '}
<Link to="/blog/tags/state-of-react">see the latest edition</Link>.
</span>
</div>

<div className={styles.legend}>
<span>
<span className={clsx(styles.glyph, styles.full)}>{GLYPH.full}</span>{' '}
Dedicated component
</span>
<span>
<span className={clsx(styles.glyph, styles.partial)}>
{GLYPH.partial}
</span>{' '}
Via prop / composition
</span>
<span>
<span className={clsx(styles.glyph, styles.none)}>{GLYPH.none}</span>{' '}
None
</span>
<span className={styles.legendNote}>
Hover a cell for the component name; click it to open the docs.
</span>
</div>

<div className={styles.tableWrap}>
<table className={styles.table}>
<thead>
<tr>
<th className={clsx(styles.headCell, styles.capCol)}>
Capability
</th>
{libs.map(lib => (
<th
key={lib.id}
className={clsx(styles.headCell, styles.libCol)}
style={{ '--lib-color': lib.color }}
title={lib.title}
>
<span className={styles.libLabel}>{lib.label}</span>
</th>
))}
</tr>
</thead>
<tbody>
{categories.map(cat => (
<React.Fragment key={cat.heading}>
<tr className={styles.catRow}>
<th
scope="colgroup"
colSpan={libs.length + 1}
className={styles.catCell}
>
{cat.heading}
</th>
</tr>
{cat.rows.map(row => (
<tr key={row[0]} className={styles.dataRow}>
<th scope="row" className={styles.capCell}>
{row[0]}
</th>
{libs.map(lib => (
<Cell key={lib.id} lib={lib} value={row[lib.idx]} />
))}
</tr>
))}
</React.Fragment>
))}
</tbody>
</table>
</div>

<details className={styles.method}>
<summary>How this was compiled — method &amp; caveats</summary>
<ul>
<li>
Rows are <strong>capabilities</strong>, not raw exports — sub-parts
are folded into their parent and internal utilities dropped, so a
column approximates each library’s user-facing catalog.
</li>
<li>
<strong>◐</strong> means the capability exists via a prop or by
composing primitives, not as a dedicated named component; it is
treated as “absent” when judging what a library uniquely ships.
</li>
<li>
Official companion packages count as first-party (Mantine{' '}
<code>@mantine/*</code>, MUI <code>@mui/lab</code> &amp;{' '}
<code>@mui/x-*</code>). shadcn/ui is a copy-paste registry powered
by Radix, cmdk, TanStack Table, and react-day-picker — not an npm
dependency.
</li>
<li>
Matched by purpose, not name (e.g. bestax <code>Badge</code> →
Mantine <code>Indicator</code> / MUI <code>Badge</code>). Competitor
links are best-effort; a few resolve to a library’s component index
rather than an exact page.
</li>
</ul>
</details>
</div>
);
}
Loading
Loading