Why semantic CSS
Krysalicss is semantic-first: the markup describes what
each element is (.button, .card,
.alert.is-danger), not how it looks. The opposite school —
utility-first frameworks like Tailwind — describes each element through
a stack of presentational atoms (p-4 text-lg flex items-center
gap-2 rounded-md bg-blue-500).
Both schools work. Krysalicss picks semantic because it wins on the axes the framework cares about the most: theme switching, readable markup, AI-agent reasoning, and refactor safety. This page explains why, then flags the helpers shipped by the framework that drift from that line — pragmatic compromises with a known cost.
The semantic premise
A class name in semantic CSS carries role. .button says "this is
a button"; .is-primary says "the primary variant of whatever I'm
attached to"; .is-disabled says "the disabled state". The
presentational details — padding, colour, radius — live in CSS,
keyed off the role, and theme-switch through custom properties.
<button class="button is-primary is-loading">Save</button>
A reader, an LLM agent, a screen reader, and a designer all extract the same meaning from that markup: it's a primary button in a loading state. The visual presentation can change radically — pill shape, square, brand colour, dark theme — without the markup moving an inch.
Where utility-first wins
It is honest to acknowledge what the other school gets right:
- Discoverability for new code. When you don't know what classes
exist,
p-4is more guessable than memorising every component name. - One-off layouts. Throwing
flex gap-2on a wrapper is faster than authoring a named role for a row that appears once. - Component-system-agnostic. Utility atoms compose without pre-existing semantic vocabulary; they fit any design system because they describe nothing.
These are real wins. They're also exactly the wins Krysalicss trades away — because the costs eat the wins back, at scale.
Why semantic wins for Krysalicss
1. Theme switching is a one-line override
:root { --kc-primary-bg: #247d59; }
.theme-dark { --kc-primary-bg: #4ade80; }
Every .button.is-primary, .card.is-primary, .alert.is-primary on
every page tracks the change. The utility-first equivalent requires
each consumer to refactor every bg-blue-500 to use a CSS variable, or
ship a parallel dark:bg-blue-400 for every utility — multiplied by
every state (hover, focus, disabled).
2. Markup stays readable
<!-- semantic -->
<article class="card is-elevated">
<h2 class="title">Heading</h2>
<p class="content">Body copy…</p>
</article>
<!-- utility-first equivalent -->
<article class="rounded-lg shadow-md p-6 bg-white dark:bg-gray-800">
<h2 class="text-2xl font-bold mb-2 text-gray-900 dark:text-gray-100">Heading</h2>
<p class="text-base text-gray-700 dark:text-gray-300 leading-relaxed">Body copy…</p>
</article>
The semantic version describes the document. The utility version describes a render. The first is what a CMS, a content authoring tool, a screen reader, or an LLM understands; the second is what a browser renders.
3. Refactor is local
A semantic codebase confines visual changes to a single SCSS file:
src/element/button/_plugin.scss. Bump the padding, every button
follows. A utility codebase confines visual changes to every consumer
of the utility — every p-4 on the site if you wanted padding to
become p-5. The migration tooling for utility-first stacks exists
precisely because this cost is real.
4. AI agents reason better about role than presentation
An LLM editing semantic markup can answer "what's the primary action
in this form?" by looking at .button.is-primary. The utility version
demands the agent decode the visual hierarchy from bg-blue-500 text-white px-6 py-3 font-semibold and infer the role. The first is a
lookup; the second is inference. Lookups don't hallucinate.
5. Accessibility surfaces through the role
.button.is-disabled pairs with [disabled] and aria-disabled in
the SCSS once, for every button. With utilities, accessibility is the
consumer's job at every callsite — and it gets forgotten.
Where Krysalicss compromises (and why)
The framework ships a src/helper/ tree of utilities. They exist for
pragmatic reasons: consumers need some escape hatches, and Bulma /
Bootstrap interoperability is real. But they drift from the semantic
line, and consumers should reach for them with their eyes open.
Utility helpers (counter-productive to the semantic line)
These helpers are pure presentational atoms with no semantic role. They name the CSS property, not the intent:
| Helper | Emitted classes | Semantic role? |
|---|---|---|
helper/position | .is-relative, .is-absolute, .is-fixed-top, .is-sticky-bottom, … | No — restate the position property |
helper/sizing | .w-25, .w-50, .h-100, .mw-100, .vh-100, … | No — restate width / height |
helper/spacing | .m-3, .p-2, .mt-4, .pe-1, … (105 classes) | No — restate margin / padding |
helper/overflow | .has-overflow-hidden, .has-overflow-x-auto, … | No — restate overflow |
helper/object-fit | .has-object-fit-cover, .has-object-fit-contain, … | No — restate object-fit |
helper/flex | .is-flex-direction-row, .is-justify-content-center, .gap-3, … | No — restate flex properties |
helper/font-size | .is-size-1 through .is-size-6 | No — restate font-size |
helper/line-height | .lh-1, .lh-sm, .lh-base, .lh-lg | No — restate line-height |
helper/text (transform/decoration/truncate slice) | .is-uppercase, .is-italic, .is-truncated, .is-nowrap, … | Partial — alignment is semantic, transform/decoration is utility |
These are exactly the helpers that, if you depend heavily on them, make Krysalicss look and feel like a utility-first framework. They defeat the markup-readability win, the refactor-safety win, and the LLM-reasoning win listed above.
Guidance:
- Use them as escape hatches for one-off cases (a layout that doesn't deserve its own component, a quick fix for legacy markup).
- Do not build your design system on them. The semantic components are the foundation; utilities are the patches.
- If you find yourself reaching for
helper/sizingandhelper/flexon every page, you're authoring a utility-first project. That's a legitimate choice, but reach for Tailwind directly — it does the utility-first job better than Krysalicss can.
Defensible helpers (still semantic-ish)
These carry role even though they emit utility-shaped classes:
| Helper | Why it stays semantic |
|---|---|
helper/color | .has-text-primary names the role in the palette (primary / warning / danger / success), not the colour value. Theme-switchable. |
helper/shadow | .has-shadow-lg names an elevation tier, not a box-shadow value. A design-system primitive. |
helper/border | .is-rounded / .is-pill / .is-bordered name shape roles, not radius values. |
helper/visibility | .is-hidden / .is-sr-only name visibility states (with accessibility semantics, not just display: none). |
helper/state | .has-state-success / .has-state-error pair the [data-state] glyph with WCAG 1.4.1 contrast. Pure semantic. |
helper/stack | .hstack / .vstack are named layout primitives (row stack, column stack) — not atomic flex utilities. |
helper/ratio | .ratio.ratio-16x9 names an aspect intent (cinema, square, portrait), not raw aspect-ratio values. |
helper/tooltip | [data-tooltip] is data-driven, attached to content rather than presentation. |
helper/interactions | .is-clickable / .is-stretched-link name affordances, not behaviours of the cursor property. |
These earn their place because they would otherwise force every component to redeclare the same role. They stay aligned with the semantic principle because the class name still carries meaning.
How to stay on the semantic line
If you're authoring a Krysalicss-based app and want to stay true to the philosophy, three habits:
- Reach for the named component first. If you find yourself
typing
.has-shadow-lg.is-rounded.p-4, ask whether that's a.cardor.boxinstead. - Treat utility helpers as escape hatches, not foundations. A
.w-100here and there is fine;.w-100on every other element is a smell. - Bake repeated utility stacks into a new component. When the same
display: flex; gap: 1rem; align-items: centerships in 30 places, add a.row-card(or whatever the role is) to your project's SCSS and use it semantically.
Further reading
- Design philosophy — the broader reasoning behind opt-in modularity and the JS-free contract.
- Architecture overview — how the SCSS module graph keeps the semantic components decoupled and the utility helpers flag-gated.
- Comparison — where Krysalicss sits next to Bulma, Picnic, Tailwind, Bootstrap on the semantic↔utility axis.