Nuxt vs Bulma: "No JavaScript Included" Is the Whole Comparison
Bulma 1.0.4 ships 96 files and zero .js, so your Vue code owns every interaction. The six components this storefront had to write for exactly that reason.
Nuxt is a Vue framework that routes, renders and builds your site. Bulma is a CSS framework that styles it. They do not compete: a Nuxt app can use Bulma, Tailwind or neither. The one fact that decides the pairing is in Bulma's own README — "There is no JavaScript included" — which makes every interactive component your framework's job.
We do not ship Nuxt here; this storefront is Next.js, so nothing below is a Nuxt benchmark. What we can measure is the half of the question that is framework-agnostic: what a CSS-only framework hands you, what it does not, and what the "does not" costs in real components. The numbers for Bulma come from version 1.0.4 of its published npm package, unpacked and measured while writing this. For the same split with a different stylesheet, read Nuxt vs Bootstrap; for the framework-to-framework question, Next.js vs Nuxt.
The two jobs, stated precisely
| Nuxt | Bulma | |
|---|---|---|
| Role | Vue application framework (routing, SSR, build) | CSS framework based on Flexbox, authored in Sass |
| Generates pages | Yes | No |
| Ships JavaScript | Yes — the Vue runtime and your app | No. Zero .js files in the package |
| Supplies component looks | No | Yes — card, modal, navbar, dropdown, tabs, menu, panel, message, pagination, breadcrumb |
| Supplies component behaviour | Yes, you write it in Vue | No |
| Can replace the other | No | No |
That third row is measurable rather than rhetorical. The bulma@1.0.4 tarball contains 96 files; filtering them for a .js extension returns 0. The package is bulma.css, bulma.min.css, a source map, and the Sass tree that generates them.
Bulma's README says why, and it is an honest design position rather than an omission:
There is no JavaScript included. People generally want to use their own JS implementation (and usually already have one). Bulma can be considered "environment agnostic": it's just the style layer on top of the logic.
In a Nuxt project you already have that implementation: Vue. So "Nuxt vs Bulma" resolves into a division of labour — Bulma owns the paint, Vue owns the state — and the real question is how much state a site like yours actually needs.
What "no JavaScript" costs, measured in components
This storefront's stylesheet also ships no JavaScript, so the bill is the same one a Nuxt-plus-Bulma build gets. Twenty-four files under src/components/ carry "use client", and six of them exist purely because a stylesheet cannot hold state:
| Component | Lines | State it owns | The Bulma element it would be styling |
|---|---|---|---|
src/components/organisms/Header.tsx | 200 | Mobile drawer open/closed, signed-in flag | navbar + navbar-burger |
src/components/molecules/FaqItem.tsx | 68 | One accordion row's open state | A card or message toggled open |
src/components/molecules/PreviewModal.tsx | 309 | Modal open, device width, iframe load | modal + modal-background |
src/components/molecules/DownloadMenu.tsx | 238 | Dropdown open, outside-click, edition choice | dropdown + dropdown-menu |
src/components/molecules/EditionPicker.tsx | 126 | Selected framework edition | tabs or a button group |
src/components/molecules/TemplateGallery.tsx | 191 | Current slide, arrow-key navigation | No Bulma equivalent — carousels aren't in the box |
Bulma's sass/components/ directory holds ten components. Seven of them — navbar, dropdown, modal, tabs, menu, panel, message — are visual states you have to drive: Bulma gives you .is-active and the CSS that responds to it, and you supply the code that puts the class there. The other three (card, breadcrumb, pagination) are static markup and genuinely free.
That is the trade in one sentence: a CSS-only framework makes the styling free and leaves the hard part priced at exactly what it costs you anywhere else.
The code a stylesheet can't contain
Here is the whole behaviour layer of one accordion row in this repo. In a Nuxt build the same thing is a ref and a :class binding, and it would be the same length:
// src/components/molecules/FaqItem.tsx
export default function FaqItem({ question, answer }: FaqEntry) {
const [open, setOpen] = useState(false);
const id = useId();
const buttonId = `${id}-button`;
const panelId = `${id}-panel`;
return (
<div className="rounded-3xl bg-gray-50">
<h3 className="m-0">
<button
type="button"
id={buttonId}
onClick={() => setOpen((o) => !o)}
aria-expanded={open}
aria-controls={panelId}
>
{question}
</button>
</h3>
<div
id={panelId}
role={open ? "region" : undefined}
aria-labelledby={open ? buttonId : undefined}
className="grid transition-[grid-template-rows] duration-300"
style={{ gridTemplateRows: open ? "1fr" : "0fr" }}
>
<div className="overflow-hidden">{answer}</div>
</div>
</div>
);
}
Three things in that snippet are the actual work, and no stylesheet of any size supplies them:
aria-expanded/aria-controls/aria-labelledby. The WAI-ARIA accordion pattern is a contract between the trigger and the panel. Bulma can style an open panel; it cannot announce one.role="region"only while open. With eight rows on/pricing, marking every collapsed panel a landmark is landmark spam for anyone navigating by region. That decision is behavioural, so it lives in the component.- The
0fr → 1frgrid transition. Height animation without a hard-coded pixel height. This is CSS, but CSS the framework doesn't write for you — Bulma's modal and dropdown simply appear.
The drawer in Header.tsx adds the two things people forget until a reviewer files them:
// src/components/organisms/Header.tsx — scroll lock + Escape, while the drawer is open
useEffect(() => {
if (!navOpen) return;
const previousOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") setNavOpen(false);
};
window.addEventListener("keydown", onKeyDown);
return () => {
document.body.style.overflow = previousOverflow;
window.removeEventListener("keydown", onKeyDown);
};
}, [navOpen]);
A Bulma navbar-burger toggles .is-active on the menu. Scroll lock, Escape-to-close, and restoring the previous overflow value on unmount are yours. In Nuxt that block is an onMounted/onUnmounted pair or a useEventListener from VueUse, with identical content.
CSS size: what the two numbers actually measure
Measured here, with gzip -9:
| Stylesheet | Raw | Gzipped |
|---|---|---|
bulma@1.0.4 css/bulma.css | 763,923 B | 67,974 B |
bulma@1.0.4 css/bulma.min.css | 677,931 B | 65,219 B |
| This storefront's one built stylesheet | 85,437 B | 15,069 B |
Read that table carefully, because the naive reading is wrong. The Bulma figures are the entire framework, which is what you get from a CDN link or a bare @use "bulma". Bulma 1.0's Sass tree is modular — importing bulma/sass/base, bulma/sass/elements/button and the two components you use produces a fraction of it. The storefront figure is the output of a generator that only emits utilities it found in the source, across 894 prerendered pages. They are different mechanisms for the same goal, and only the second one is automatic.
The more interesting 1.0 change is that Bulma now emits CSS custom properties: bulma.css contains 7,821 lines mentioning a --bulma-* variable. That means retheming no longer requires a Sass build at all — you override custom properties at runtime, the same shape as this repo's token block:
/* src/app/globals.css — 133 lines total, tokens and all */
@theme {
--color-primary: #465fff;
--color-primary-600: #3641f5;
--color-gray-900: #101828;
}
If you adopt Bulma under Nuxt today, use the custom properties rather than the Sass variables unless you specifically need compile-time output. It removes a dependency (sass) and a class of silent failure where a variable set after the import does nothing.
The part neither framework does: contrast
A CSS framework hands you a colour palette, which feels like it hands you accessibility. It does not. This repo's own Lighthouse pass caught a defect worth repeating because it is the exact failure mode a palette invites: BlogTag rendered text-primary (#465fff) on bg-primary-25 (#f2f7ff) at 12px, which measures 4.49:1 — short of the 4.5:1 AA threshold by a hundredth. The fix was one token (text-primary-600), and no framework was going to find it.
Brand-tinted small text on a brand-tinted background is where this happens, in Bulma's is-light colour variants exactly as much as in ours. Measure the pairs you invent; the palette only guarantees the ones it shipped.
A decision rule
| If you... | Choose |
|---|---|
| Want finished component styling and will write the state yourself | Nuxt + Bulma. The division of labour is clean and documented |
| Want a bespoke design with CSS generated only for what you used | Nuxt + Tailwind, or a Tailwind-based Vue kit |
| Want interactive components you don't have to wire | A Vue component library (Nuxt UI, PrimeVue), not a CSS-only framework |
| Are already on Vue 3 and value no build step for theming | Bulma 1.x via its custom properties |
| Are building a landing page rather than an app | A finished template; the stylesheet choice is already made for you |
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Bulma dropdown or modal never opens | No JavaScript ships with Bulma; nothing adds .is-active | Bind :class="{ 'is-active': open }" to a Vue ref |
| Navbar burger does nothing on mobile | Same cause — the burger is markup, not a widget | Toggle .is-active on both the burger and the navbar-menu |
| Sass variable override ignored | Overrides were set after @use "bulma" | Set them in the with (...) configuration, or override --bulma-* custom properties instead |
| Bulma's reset fights another framework's preflight | Two global resets on the same page | Use one, or scope Bulma to a container |
| Theme change needs a full rebuild | Theming through Sass variables rather than custom properties | Override the --bulma-* properties at runtime |
| New design token does nothing in dev | A theme file read once at startup isn't hot-reloaded — this repo hit it with Tailwind v4 and Turbopack | Restart the dev server before debugging anything else |
Frequently asked questions
Can I use Bulma with Nuxt?
Yes. Install bulma, import its CSS (or its Sass, with sass installed) from your Nuxt config or a global stylesheet, and write the interactive bits as Vue components. Nothing in Bulma assumes a framework; nothing in Nuxt assumes a stylesheet.
Is Nuxt better than Bulma? The question compares a page generator with a style layer. A Nuxt site needs some styling solution and Bulma is a reasonable one, so picking Nuxt never rules out Bulma.
Does Bulma really include no JavaScript?
Yes — its README states it plainly, and the published 1.0.4 package contains zero .js files. Interactive components are shipped as CSS that responds to an .is-active class you apply yourself.
Is Bulma or Tailwind the better fit under Nuxt? Bulma if you want pre-designed components and will accept a recognisable look. Tailwind if you want a token system and CSS generated only for what you used — the 15,069-gzipped-bytes figure above is that mechanism's output for 894 prerendered routes, not a promise about your project.
Skip the stylesheet decision
If the goal is a finished page rather than a styling philosophy, ASoc Sentinel, ASoc Signal and ASoc Sterling are landing sets with the tokens, dark mode and accordion behaviour already written and measured.
Browse the sets: Next.js landing page templates and Tailwind landing page templates.
