Skip to main content
ASoc
Comparison

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.

The ASoc Team9 min read

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

NuxtBulma
RoleVue application framework (routing, SSR, build)CSS framework based on Flexbox, authored in Sass
Generates pagesYesNo
Ships JavaScriptYes — the Vue runtime and your appNo. Zero .js files in the package
Supplies component looksNoYes — card, modal, navbar, dropdown, tabs, menu, panel, message, pagination, breadcrumb
Supplies component behaviourYes, you write it in VueNo
Can replace the otherNoNo

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:

ComponentLinesState it ownsThe Bulma element it would be styling
src/components/organisms/Header.tsx200Mobile drawer open/closed, signed-in flagnavbar + navbar-burger
src/components/molecules/FaqItem.tsx68One accordion row's open stateA card or message toggled open
src/components/molecules/PreviewModal.tsx309Modal open, device width, iframe loadmodal + modal-background
src/components/molecules/DownloadMenu.tsx238Dropdown open, outside-click, edition choicedropdown + dropdown-menu
src/components/molecules/EditionPicker.tsx126Selected framework editiontabs or a button group
src/components/molecules/TemplateGallery.tsx191Current slide, arrow-key navigationNo 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:

  1. 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.
  2. 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.
  3. The 0fr → 1fr grid 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:

StylesheetRawGzipped
bulma@1.0.4 css/bulma.css763,923 B67,974 B
bulma@1.0.4 css/bulma.min.css677,931 B65,219 B
This storefront's one built stylesheet85,437 B15,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 yourselfNuxt + Bulma. The division of labour is clean and documented
Want a bespoke design with CSS generated only for what you usedNuxt + Tailwind, or a Tailwind-based Vue kit
Want interactive components you don't have to wireA Vue component library (Nuxt UI, PrimeVue), not a CSS-only framework
Are already on Vue 3 and value no build step for themingBulma 1.x via its custom properties
Are building a landing page rather than an appA finished template; the stylesheet choice is already made for you

Troubleshooting

SymptomCauseFix
Bulma dropdown or modal never opensNo JavaScript ships with Bulma; nothing adds .is-activeBind :class="{ 'is-active': open }" to a Vue ref
Navbar burger does nothing on mobileSame cause — the burger is markup, not a widgetToggle .is-active on both the burger and the navbar-menu
Sass variable override ignoredOverrides were set after @use "bulma"Set them in the with (...) configuration, or override --bulma-* custom properties instead
Bulma's reset fights another framework's preflightTwo global resets on the same pageUse one, or scope Bulma to a container
Theme change needs a full rebuildTheming through Sass variables rather than custom propertiesOverride the --bulma-* properties at runtime
New design token does nothing in devA theme file read once at startup isn't hot-reloaded — this repo hit it with Tailwind v4 and TurbopackRestart 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.

Keep reading

Comparison7 min read

One-Time Payment vs Subscription: What the Webhook Handler Has to Know

Our entitlement engine is 75 lines with two status states; our LemonSqueezy webhook recognizes two event names. That's not a shortcut — it's the real cost gap between the two pricing models.

Read more
Comparison8 min read

Playwright vs Vitest: Why This Repo Runs Both for Unrelated Jobs

Vitest is a committed dependency gating 305 tests on every push. Playwright is deliberately not a dependency at all — installed ad hoc for a script that runs a few times a year.

Read more
Comparison9 min read

Qwik vs. Next.js: Resumability vs. Not Shipping the Component

Qwik ships near-zero JS through resumability; this site's RSC split already does that for 9 of 10 home-page sections. Where the real cost still lands: one accordion.

Read more