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-4 is more guessable than memorising every component name.
  • One-off layouts. Throwing flex gap-2 on 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:

HelperEmitted classesSemantic 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-6No — restate font-size
helper/line-height.lh-1, .lh-sm, .lh-base, .lh-lgNo — 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/sizing and helper/flex on 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:

HelperWhy 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:

  1. Reach for the named component first. If you find yourself typing .has-shadow-lg.is-rounded.p-4, ask whether that's a .card or .box instead.
  2. Treat utility helpers as escape hatches, not foundations. A .w-100 here and there is fine; .w-100 on every other element is a smell.
  3. Bake repeated utility stacks into a new component. When the same display: flex; gap: 1rem; align-items: center ships 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.