What actually lands in your bundle when you import one component

A weight breakdown of one design system — what a component costs, what a second theme costs, and why CSS and JS do not correlate.

“It’s only a button” is how bundle budgets die. The honest question is not what a component weighs but what arrives with it — the framework glue, the styles of everything the sheet was told to emit, the second theme nobody asked for. This post is the answer for one design system, measured rather than asserted.

Every figure below was taken on 2026-08-29 against core 0.38.0 and preset 0.15.0. The method is stated with each one, because a number without a method is a claim, not a measurement.

Two costs, and they are unrelated

A component costs you twice: once in CSS, once in JavaScript. The instinct is that a heavy component is heavy in both. It is not.

ComponentStyles it adds to the sheetIts own code
GrCard223 B1 870 B
GrDialog318 B9 789 B
GrDataTable705 B16 243 B
GrSelect2 335 B19 819 B

Gzipped, framework external. GrDialog costs 30× more code than styles; GrSelect only 8×. The reason is boring and worth knowing: styles scale with how many visual states a component has, code scales with how much behaviour it carries. A dialog is a plain box with a focus trap. A select is a plain box with a listbox, filtering, keyboard navigation and a floating panel.

The practical consequence: you cannot estimate a component’s weight by looking at it. The lightest in this package is 1 455 B and the heaviest 19 819 B — a spread of 13.6×, and the visual difference between them is not 13.6× of anything.

What the first component actually costs

Here is where “it’s only a button” is genuinely wrong, and where it is genuinely right.

SheetGzipped
One component, one theme7 504 B
One component, two themes8 250 B
Every component of the core, two themes17 430 B

The first component costs 7 504 B and the remaining 76 cost 9 180 B between them. That is not a paradox: the first one pays for the foundation — the token declarations, the base layer, the preflight — and everyone after it pays only for its own utilities.

So “it’s only a button” is wrong about the first button and roughly right about the second. If you are adding a design system for one component, you are paying for a design system. If you are adding the tenth, you are paying for a component.

The second theme costs 746 bytes

Not 746 kilobytes, and not a second sheet: 746 B, because a theme is a block of custom-property values under a selector, not a copy of the styles. Every rule that uses var(--gr-bg) is emitted once regardless of how many themes define it.

This is the part people mis-budget in both directions. Teams drop dark mode to save weight it never cost, and teams add six brand themes assuming they are free — they are, in CSS, and they are not free in review, contrast checking or design decisions. The byte count is the least of it.

Where granular imports actually help

The package declares 136 export paths and the core carries 77 components. Importing from a subpath rather than the root barrel is what keeps the other 76 out:

ts
// The whole barrel is reachable from here — tree-shaking has to prove
// what is unused, and it cannot always prove it through re-exports.
import { GrButton } from '@feugene/granularity'

// One component and its own graph. Nothing to prove.
import { GrButton } from '@feugene/granularity/components/GrButton'

Both lines work. The second one is smaller in every bundler and identical in the best one, which is the whole argument for it: the win is not that it is faster when everything goes right, it is that it does not depend on everything going right.

The CSS side is not tree-shaken at all — it is generated. The sheet contains what the config was told to emit, which is why narrowing components in uno.config.ts is the one place where granularity costs you attention:

ts
const granular = {
  providers: [granularityProvider],
  components: [{ provider: '@feugene/granularity', names: ['GrButton', 'GrCard'] }],
  themes: { names: ['light', 'dark'] },
}

Leave components out and you get all of them — 17 430 B instead of 8 473 B. That is a real difference and a bad default to inherit by accident.

What this does not tell you

Three honest limits.

These are gzipped bytes, not parse time. 19 819 B of GrSelect costs more to execute than 19 819 B of markup, and neither number tells you about the third render.

The framework is external in every JS figure. Vue is not in these numbers, because it is not in them for you either — you already have it. If you do not, add it, and the design system stops being the interesting part of your budget.

Zero runtime dependencies is a property of the core, not of the family. The core declares 0 runtime dependencies; companion packages that wrap a third-party editor or highlighter declare theirs, and they are listed on each package page.

The one number worth remembering

Not 7 504, and not 13.6×. It is this: the first component of a design system costs about as much as all the rest together. Budget the decision, not the import.

Components in this article