Migrating from plain Bulma classes
Most React apps that use Bulma never picked a wrapper library. They import Bulma's stylesheet and write its classes on plain JSX:
<div className="columns is-mobile">
<div className="column is-half">
<a className="button is-primary is-large" href="/signup">
Sign up
</a>
<p className="has-text-centered mt-4">No card needed</p>
</div>
</div>
That works, and it stays working. What you give up is the part React is good at: typed props,
autocomplete on every color and size, and components you can find in your editor instead of
class strings you have to remember. The bulma-classes codemod moves that markup onto
@allxsmith/bestax-bulma:
<Columns isMobile>
<Column size="half">
<Button as="a" color="primary" size="large" href="/signup">
Sign up
</Button>
<Paragraph textAlign="centered" mt="4">
No card needed
</Paragraph>
</Column>
</Columns>
bestax renders Bulma's own classes, so the page renders the same. The codemod converts an
element only when the component renders the markup the element did: same tag, same
classes, same attributes, with only the order of the classes free to change. Everything else
stays as you wrote it, with a TODO(bestax-migrate) comment when there's a decision to make.
Run the codemod
# Preview the changes and the TODO report without writing anything
pnpm dlx bestax-migrate bulma-classes src/ --dry
# Apply it
pnpm dlx bestax-migrate bulma-classes src/
(npx bestax-migrate … works the same.) The flags are the same as for the library sources:
--print, --extensions, --css bestax|bulma|keep for the stylesheet target, and
--no-deps to leave package.json alone.
Your styling stays yours. This source defaults to --css keep, because your Bulma stylesheet
already styles every class a converted element renders: your stylesheet imports, your Sass and
your Bulma version stay as they are, and package.json just gains @allxsmith/bestax-bulma.
Moving to Bulma v1 is its own step, whenever you want it. --css bestax swaps your stylesheet
import for bestax's bundle, moves a Bulma 0.9 Sass setup onto v1's @use form, bumps a
pre-1.0 Bulma and swaps node-sass for sass; --css bulma does the same but keeps Bulma's
stock stylesheet.
What converts
- Components:
button,buttons,columns,column,container,section,heroand its parts,title,subtitle,box,content,block,notification,tag,tags,leveland its parts,mediaand its parts,delete,progress,footerandtablebecome their bestax components, with their modifier classes as props (is-primary→color="primary",is-half→size="half"). - Helper classes become helper props on those components (
mt-4→mt="4",has-text-centered→textAlign="centered"), and on the plain tags bestax wraps:<p>becomesParagraph,<span>becomesSpan, and so on. - Your own classes stay in
className, which every bestax component passes through.
The full tables, class by class, are in the migrate skill's component map and prop map.
A couple of results look odd until you see why:
<h2 className="title is-4">becomes<Title as="h2" className="is-4">, not<Title size="4">. bestax picks the heading tag fromsize, sosize="4"would render an<h4>.<div className="is-flex mt-4">doesn't change at all. bestax has no plain<div>component, and the classes are valid Bulma, so there is nothing to do and nothing to flag.
What it leaves for you
Anything that would change the markup stays as written:
- Computed classNames.
clsx(...), ternaries and templates with expressions are flagged with the component the element would become (dynamic-class:<Target>). Converting them means turning each condition into a prop, which is quick by hand and risky to guess at. - Components that render parts of their own.
Cardwraps stray children in.card-content,Navbaradds navigation roles, bestax's form controls render their own.fieldand.control, and so on. These families are flagged once each (family:<class>) and converted by hand. - Elements bestax would render differently: a
refon a component that doesn't forward one, a spread, a tag the component can't render (<div className="section">), an attribute the component reads as a prop, or an element that is the only child of another component, which may clone it (<Link><a className="button">). - Next.js App Router projects. A file without
'use client'is left alone (rsc), because bestax's components are client components, and in an App Router project any module can render as a server component, not only those underapp/. Files underpages/convert. - Files React doesn't render the usual way: a component styled with
<style jsx>, whose scoped styles would miss a converted element (styled-jsx), JSX that renders through Preact or another runtime (jsx-runtime), and CommonJS files (imports).
The TODO report
Every TODO sits on the statement around the element, and the run ends with a report grouped by rule. The ones you'll see most:
| What | What to do |
|---|---|
dynamic-class:<Target> | Convert by hand: each cond && 'is-x' becomes the prop (isX={cond}) |
family:<class> | Rebuild that block from the component's docs page, parts and all |
tag:<Target> | Change the tag if you want the component, or keep the markup |
defaults:Delete | Add type="button" and a real aria-label to the .delete button, then re-run |
ref:<Target> | Keep the element; the component won't pass the ref through |
rsc | Add 'use client' if the file can be a client component, then re-run |
legacy:tile | Rebuild tiles with Grid and Cell; see the Bulma 0.9 → 1 guide |
Every rule, with a recipe, is in the skill's unmappables reference. It is safe to re-run the codemod after fixing some of them: converted elements are components now, and a TODO it already wrote is never written twice.
The bestax-migrate Agent Skill runs the codemod and works through the TODOs from the same
references:
npx skills add https://github.com/allxsmith/bestax --skill bestax-migrate
Finish the migration
- Install: the codemod rewrote
package.json; apply it withnpm install(or pnpm/yarn). bestax-bulma needs React 18 or 19, and the report says so if you're older. - Stylesheet: under the default there's nothing to do. If you ran with
--css bestaxfrom Bulma 0.9, read the Bulma 0.9 → 1 guide for the styling changes that aren't code. One deliberate difference inbestax.css: its$primaryis bestax blue rather than Bulma's turquoise. Keep the stock look with--css bulma, or set your own with the--bulma-primary-*CSS variables. - PurgeCSS: if your build runs it, the report says so. A converted element's classes now come from bestax's code, some of them built from props at runtime, so PurgeCSS has to scan bestax and safelist those patterns; Optimizing CSS has the config.
- Snapshot tests: converted elements list their classes in a different order (bestax's own first), so snapshots that compare class strings will change while the page doesn't. Review the diff and update them.
- Verify: typecheck, build, and look at the app.
Coming from a library instead?
If the app imports react-bulma-components, rbx or bloomer, use that source instead:
react-bulma-components, rbx,
bloomer. An app can run more than one: migrate the library first, then run
bulma-classes over what's left.