# Atoms, layers, types, tokens: a new CSS methodology built on CSS Modules

> Source: <https://dev.to/a-dev/atoms-layers-types-tokens-a-new-css-methodology-built-on-css-modules-35ke>
> Published: 2026-07-20 18:36:22+00:00

It's not like I dislike Tailwind, but I can't say that I'm in love with it either. I'm keen on the "atomic CSS" part of it, but reading all this mass of short class names in the HTML is a bit of a hard job, don't you find? Especially when it comes from an AI model and the diff is really big. Also, I've noticed that models feel free to use anything Tailwind offers, and it's hard to harness them properly.

That was the reason I decided to look for another way to write CSS: one that gives me flexibility, sets clear architectural boundaries for how styles are organized and composed, and is easy to read and maintain. I chose CSS Modules because it can be harnessed and checked very well. The authoring remains ordinary CSS, plus the `composes`

extension when a project composes on the CSS side, so every diff is plain CSS you can compare line by line. The great part: you can easily debug it. You know, browser DevTools are really good for that.

So, my claim upfront: once the project plumbing exists, a small set of conventions on top of CSS Modules gives you a compact workflow for common component styling: a shared style API, typed variants, observable state, deterministic local overrides, and semantic color theming. It also gives you readable diffs, useful names in DevTools, and compile-time errors instead of a silent `undefined`

in the class string.

What I'm proposing here is a new CSS methodology. A young one, to be honest: this article is its first full write-up, a beginning rather than a finished spec. But it's aiming for the same shelf where BEM, OOCSS, SMACSS, and others sit, and every system on that shelf started the same way: as somebody's blog post or conference talk. Mine proposes an architecture too, down to how a single module file is laid out. I respect all of them, but they were invented before components, and most of their rules exist to solve one big problem: scoping styles through naming discipline.

CSS Modules already scopes styles mechanically, so all that discipline can be spent on better things: design tokens, cascade layers, compile-time types. The old methodologies taught us how to survive the global namespace. Mine gets to assume the problem is solved and build on top. Atomic CSS sits on that shelf too, but on a different branch: I'm not replacing it, I borrowed the atoms idea from it.

It's real CSS all the way down, just better behaved:

`cx(layout.padM, typography.h1)`

.`!important`

.`data-*`

state you can flip right in the Elements panel, instead of clicking through the app to reproduce a bug.And you don't have to adopt it by hand: `npx skills add a-dev/skills --skill css-modules-setup --skill css-modules`

installs [the two canonical skills](https://github.com/a-dev/skills/tree/main/skills/frontend) that let an AI agent set the system up and then code in it with you. You read the article, your agent reads the manual.

And to be fair from the start, there are two things Tailwind still does better: zero-setup prototyping (this system has a setup cost, paid once per project) and raw CSS bytes at scale. I can live with both trade-offs, because honestly, prototypes are now easier to make entirely with AI and no one cares what's inside; we only need them to test ideas. This methodology optimizes for maintenance and debugging rather than minimum CSS bytes. If stylesheet size or parse cost actually matters in your project, measure before you commit.

Also, Tailwind gives you names, so you don't have to invent them (naming is genuinely hard). I tried to alleviate this pain with a small set of conventions that both you and the AI can follow.

Some people will say that they prefer Tailwind because everything exists in one file. I'm not that person, because that's exactly what turns PR diffs into soup, especially when it comes to a basic UI component like a button with a lot of variants. I like CSS, and I'm not afraid to say it.

Note that the attached implementation isn't universally applicable for now: it uses Vite and React. The principles can apply elsewhere, but another framework or a vanilla project needs its own setup and verification.

The architecture is simple: global plumbing establishes the selected layer order, color roles, and generated types. A project-defined shared API sits above it. Local component modules consume and override that API.

**Global:** color tokens + color-scheme + recorded layer topology + generated types **→**

**Shared API:** atoms module/modules, shared ui-components **→**

**Local styles:** unlayered feature modules in the reference setup.

Here `atoms`

, `ui`

, and `#styles`

are just the names I picked. Nothing in the system depends on them. A project can use one shared module, role-based modules (layout/typography/spacing/etc.), or a different map entirely.

The names aren't the point. The point is that the shared boundary is written down and its place in the cascade stays predictable. Local styles then follow whatever override strategy the project chose.

This methodology doesn't prescribe a spacing or sizing scale. Those values and tokens belong to the project's design system; the methodology only says how styles are organized, composed, checked, and overridden.

In my reference setup, the first piece of plumbing is the `@layer atoms`

wrapper. It's the mechanism that makes all the overrides painless.

The rule from the CSS spec is simple: for normal author declarations, unlayered styles beat layered ones, regardless of selector specificity. So the reference setup takes three steps. First, declare the layer order once in your global CSS:

```
/* src/shared/styles/index.css */
@layer reset, base, atoms, ui;
```

Treat this as a starting map. A project can rename, split, or add layers, and module boundaries don't have to match layer boundaries one-to-one, as long as there is a single recorded order and it's clear who owns what.

Second, wrap shared styles in their layers: atoms in `@layer atoms`

, UI primitives like our button in `@layer ui`

. Third, don't wrap app components at all: they stay unlayered, so they always win.

That's the whole trick: shared styles are the floor, local styles are the ceiling. Take a local `.title`

that composes an `h1`

atom with `margin: 0`

, then overrides it with `margin-top: 9px`

. Both selectors are single classes with equal specificity, and without layers the winner would depend on the source order in the final bundle. Fragile and scary. With layers, the local rule wins deterministically: you don't have to count specificity or reach for `!important`

, and import order stops mattering.

One nuance: layers give you determinism between layers, not inside one. If you combine two atoms that set the same property, the usual source-order rule still applies, and the later one wins silently. In practice, role-based naming makes such clashes rare.

The other part of the global plumbing was sorting out the mess with colors in the project's styles. After some trial and error, I came to the "severance" of color tokens into two tiers: palette and semantic color roles. If you use Tailwind, you know this huge palette. It's good (you have a lot of options), but in a real project all those options erode the standard (good design is, above all, repeatability and consistency). Add a light/dark theme to it, and the remaining consistency falls apart in a moment.

In `palette.css`

I keep vars for all the colors in lists of tones:

```
:root {
  --color-gray-50: oklch(96.6% 0.005 240deg);
  --color-gray-100: oklch(93% 0.008 240deg);
  --color-gray-200: oklch(87% 0.009 240deg);
  --color-gray-900: oklch(18% 0.015 240deg);
  --color-blue-400: oklch(70% 0.14 250deg);
  --color-blue-500: oklch(62% 0.17 250deg);
  --color-blue-600: oklch(54% 0.18 250deg);
  /* ... */
}
```

And in `colors.css`

I keep vars for the colors that are used in the project, mapped to the palette:

```
:root {
  --color-text-primary: light-dark(var(--color-gray-900), var(--color-gray-100));
  --color-panel-bg: light-dark(var(--color-gray-50), var(--color-gray-900));
  --color-action-bg: light-dark(var(--color-blue-600), var(--color-blue-500));
  --color-action-text: light-dark(var(--color-gray-50), var(--color-gray-50));
  --color-accent-bg: light-dark(var(--color-blue-400), var(--color-blue-600));
}
```

Components consume the semantic roles and never touch the raw palette directly. Other token families can exist too; this article only pins down color.

`light-dark()`

reads the active `color-scheme`

, so the global setup needs to connect theme selection to it:

```
/* Reference topology; use the project's selected layer map. */
@layer reset, base, atoms, ui;

@import "./vars/colors.css"; /* @import "./palette.css" exists only inside colors.css to keep the dependency chain correct */

@layer base {
  html {
    color-scheme: light dark;
  }

  html[data-theme="light"] {
    color-scheme: light;
  }

  html[data-theme="dark"] {
    color-scheme: dark;
  }
}
```

Who sets `data-theme`

? Application bootstrap code, an SSR-safe theme script, or the framework integration. That code also owns persistence and the initial render. Component modules never touch the theme themselves.

The mapping doesn't replace runtime checks either: you still have to verify contrast, forced colors, and the first rendered theme in the environments you actually support.

First of all, I want to have type safety for my CSS Modules. I use Vite, and it has some problems with CSS Modules. I went through several libraries and even started to make my own, but ended up choosing [vite-css-modules](https://github.com/privatenumber/vite-css-modules), the best pick for now. In a nutshell, it fixes `composes`

imports, removes duplicates (yes, Vite creates them easily), fixes HMR and some other bugs, and renders classes in a way that works well. I recommend reading the README of this library, it has some good explanations and examples.

The other thing is a bit opinionated, but I think I'm right in this opinion 😉. In CSS I use kebab-case (`.icon-light-s`

); in a component I use camelCase (`styles.iconLightS`

).

`camelCaseOnly`

is what makes that split real: only the camelCase key exists at runtime.

Generated declarations expose the same shape to TypeScript. Keep in mind that Vite doesn't typecheck, so the error appears only when `tsc --noEmit`

runs.

Here's a complete Vite example. Merge it into your existing config; don't replace the plugin array.

``` python
// vite.config.ts
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import { patchCssModules } from "vite-css-modules";

export default defineConfig({
  css: {
    devSourcemap: true,
    modules: {
      localsConvention: "camelCaseOnly",
    },
  },
  plugins: [
    react(),
    patchCssModules({
      generateSourceTypes: true,
      declarationMap: true,
    }),
  ],
});
```

Now `my-component.module.css.d.ts`

describes the exported keys, and its declaration map connects them back to the CSS source. A typo then fails under `tsc --noEmit`

:

```
styles.iconLigthS;
// Property 'iconLigthS' does not exist.
```

I put generated declarations in `.gitignore`

, so a fresh clone has none. The dev server regenerates them during development; CI and package lifecycle scripts must generate them before typechecking.

For npm, that order can look like this:

```
{
  "scripts": {
    "css:generate": "vite-css-modules",
    "css:types": "tsc --noEmit",
    "css:check": "npm run css:generate && npm run css:types"
  }
}
```

The setup skill writes the equivalent commands for the project's package manager. [The reference fixture](https://github.com/a-dev/skills/tree/main/skills/frontend/css-modules-setup/fixtures/vite-react) runs generation, `tsc --noEmit`

, and a production build.

Everything started from the idea of making atoms in CSS Modules. I wanted to have a set of classes that can be used in any component, but can also be easily overridden if needed. That isn't so easy, because importing them is always a problem, but I wanted to find a simple way.

For this, we should create a place where we put our "atoms" and global/common styles that we can use anywhere. I used the `src/shared/styles/`

folder and created an alias for it: I chose `#styles`

. It's probably not the most popular choice (you'll more commonly see `@/shared/styles`

), but it's shorter, and soon you'll see why it works well.

In this folder I created:

```
// src/shared/styles/index.ts

export { default as layout } from "./layout.module.css";
export { default as typography } from "./typography.module.css";
export { default as utils } from "./utils.module.css";
export { cx, type ClassValue } from "./lib/cx";
export { cssVars } from "./lib/css-vars";
// ... and so on, whatever you need. Or only 'atoms.module.css'
```

Take this entry point as an example rather than a required list. An atom is simply a reusable class exposed through the project's shared style API; it can contain one declaration or several related ones.

Then, for example, I created classes like:

```
/* src/shared/styles/typography.module.css */

@layer atoms {
  .h1 {
    margin: 0;

    font-size: var(--fs-xxl);
    font-weight: 600;
    line-height: 1.27;
    color: var(--color-text-primary);
  }
}

/* src/shared/styles/layout.module.css */

@layer atoms {
  .pad-m {
    padding-inline: 20px;
    padding-block: 16px;
  }

  .bg-top-gradient {
    background: linear-gradient(90deg, var(--color-action-bg) 0%, var(--color-accent-bg) 100%);
  }
}
```

The values here are only examples. Use the spacing and sizing system your project already has, whether that means tokens, raw values, a grid, or something else.

Then you can use them in your components like this:

``` js
import { layout, typography, cx } from "#styles";

function MyComponent() {
  return (
    <div className={cx(layout.padM, layout.bgTopGradient)}>
      <h1 className={typography.h1}>Hello World</h1>
    </div>
  );
}
```

I personally prefer some verbosity in naming, and that's why I intentionally don't use `p4`

or anything else common in Tailwind class names, but you're free to do it.

And as you may have noticed, I import the `cx`

function, which is a simple helper for combining classes; you can use any other library for this, like `classix`

/`clsx`

/`classnames`

. I prefer to import it as `cx`

and not care how it actually works inside. And I use it only for combining component variants, never for dynamic state; I'll tell you why a bit further on.

So what if we need to add some specific design and want to use local styles? This is the moment when it all becomes really great, because you can use local styles and shared atom styles together with `composes`

. (The `composes`

keyword itself is widely supported; importing across files through an alias is a build-tool concern, which is why the setup step verifies it.)

```
/* src/.../my-component/my-component.module.css */
.root {
  composes: pad-m bg-top-gradient from "#styles/layout.module.css";
  grid-column: 1 / 3;
}

.title {
  composes: h1 from "#styles/typography.module.css";
  margin-top: 9px;
}
```

Pay attention to the naming: in `composes`

you write class names exactly as they are in the CSS source: kebab-case (`pad-m`

), not camelCase (`padM`

). This line is resolved by the CSS pipeline itself, without any JS imports, so the camelCase export convention doesn't apply here.

And then in your component:

``` python
import styles from "./my-component.module.css";

function MyComponent() {
  return (
    <div className={styles.root}>
      <h1 className={styles.title}>Hello World</h1>
    </div>
  );
}
```

An important detail: `composes`

doesn't create a CSS rule with additional declarations in it; no, it adds a class to the HTML element. In the rendered HTML it looks like this:

```
<div class="root pad-m bg-top-gradient">
  <h1 class="title h1">Hello World</h1>
</div>
```

The exact runtime class names depend on the build configuration, but you get the idea.

As you can see, you can recreate Tailwind-like classes, but in a more maintainable way. Honestly, I don't think you should do it, because the philosophy of this approach is a bit different: stay close to real CSS, but also have atoms or shared styles that can be used in components and are easy to read and debug. And if you need to override something, you can do it in your component's local styles, and it looks like the real CSS way, just with some smart helpers. The `@layer`

boundary is what makes that local override reliable.

When a local style should graduate into that shared API is also a project decision. My conservative default: when a second component needs it for the same reason. Your project can pick a different rule.

That was the core of this methodology. The next question is component variants and state.

This one is really opinionated, but it pays off: represent state through the channel that owns its meaning.

| Input or state | Representation | Selector |
|---|---|---|
| Closed design variant | Typed CSS Module lookup | `.variantPrimary` |
| Native HTML state | Native attribute or pseudo-class |
`:disabled` , `[open]`
|
| Meaningful accessibility state |
`aria-*` value |
`[aria-pressed="true"]` |
| Private boolean state | Presence-based `data-*`
|
`[data-loading]` |
| Headless-library state | Existing library attribute | Library-defined |
| Continuous runtime value | Private CSS custom property | `var(--_progress)` |

Use `data-*`

only when the state has no native or accessibility representation. If the platform or a headless library already exposes it, style that source directly.

Here's how it looks for a button:

``` python
// ui/button.tsx
import type { ComponentPropsWithoutRef } from "react";
import { cx } from "#styles";
import styles from "./button.module.css";

type Variant = "primary" | "secondary" | "outline";
type Size = "s" | "m" | "l";

type ButtonProps = ComponentPropsWithoutRef<"button"> & {
  variant: Variant;
  size: Size;
  loading?: boolean;
};

const VARIANT_CLASS = {
  primary: styles.variantPrimary,
  secondary: styles.variantSecondary,
  outline: styles.variantOutline,
} satisfies Record<Variant, string>;

const SIZE_CLASS = {
  s: styles.sizeS,
  m: styles.sizeM,
  l: styles.sizeL,
} satisfies Record<Size, string>;

export function Button({
  variant,
  size,
  loading = false,
  className,
  disabled,
  children,
  ...buttonProps
}: ButtonProps) {
  return (
    <button
      {...buttonProps}
      className={cx(styles.root, VARIANT_CLASS[variant], SIZE_CLASS[size], className)}
      disabled={disabled || loading}
      data-loading={loading || undefined}
      aria-busy={loading || undefined}
    >
      {children}
    </button>
  );
}
```

One trap here: `loading || undefined`

. If you write `data-loading={false}`

, React keeps `data-loading="false"`

in the DOM. `[data-loading]`

still matches it. Passing `undefined`

removes the attribute.

That presence rule is only for boolean `data-*`

. ARIA uses values: `aria-pressed="false"`

is meaningful and must stay in the DOM.

This button becomes disabled while loading. That's a deliberate UX decision, not a side effect of `pointer-events`

.

A product that must preserve focus can use `aria-disabled`

and guard its event handlers instead. Either way, `aria-busy`

only announces the state; it doesn't make a complete loading UX by itself.

Also note the types: the map and the union can't drift apart. Add `"danger"`

to `Variant`

, and TypeScript underlines `VARIANT_CLASS`

until you add both the class and the entry.

Extending a component becomes a mechanical, compiler-guided task (try to get this from a class string).

And in the CSS module:

```
/* ui/button.module.css */
@layer ui {
  .root {
    border: none;
    /* ... */
  }

  .root[data-loading] {
    opacity: 0.5;
  }

  .root:disabled {
    cursor: not-allowed;
  }

  .variant-primary {
    background-color: var(--color-action-bg);
    color: var(--color-action-text);
  }

  .variant-secondary {
    background-color: var(--color-panel-bg);
    color: var(--color-text-primary);
  }

  .variant-outline {
    color: var(--color-action-bg);
    background: transparent;
  }

  .size-s {
    padding: 4px 8px;
    font-size: var(--fs-s);
  }

  .size-m {
    padding: 8px 12px;
    font-size: var(--fs-m);
  }

  .size-l {
    padding: 12px 16px;
    font-size: var(--fs-l);
  }
}
```

A small perk that I really enjoy: state debugging now happens right in the Elements panel. Open DevTools, add `data-loading`

to the button by hand, and you instantly see the loading style, without needing React DevTools or clicking through the app to reproduce the state. It works in the other direction too: when a bug report comes in, you can rebuild the broken state attribute by attribute.

Also, I insist on short names: instead of BEM or repeating the component name, use just `root`

, `icon`

, `label`

, `header`

, `item`

, `meta`

, etc. If you have a component with several variants, you can use `variant-primary`

, `variant-secondary`

; for sizes, `size-s`

, `size-m`

, `size-l`

(yes, I prefer an alpha size scale: it's flexible and easy to read). And if you have a component with several parts, you can use `header`

, `body`

, `footer`

, etc.

And even a year after you last looked at a component, you can quickly tell what's going on in it, because the names describe roles and the state is visible right in the markup.

Here I need to be precise: I use "enforced" only when a command can fail and the project actually runs it. A sentence in a skill is still a rule, but it isn't a gate. The setup skill ships with a read-only audit. If a project opts into enforcement, it can also install ESLint, Oxlint, Stylelint, and cross-file checks. A checker in the skills repository enforces nothing until the project installs it and runs `css:check`

at error severity. I started asking every rule the same question: if I break you, who complains?

*I misspell a CSS Module class or add "danger" to the variant union but forget its map entry.*

`TYPECHECK FAILS`

. Generated declarations reject the missing key, and `satisfies Record`

rejects the incomplete map when [ css:types](https://github.com/a-dev/skills/blob/main/skills/frontend/css-modules-setup/SKILL.md) runs.

*I break the #styles alias or remove declaration generation.*

`AUDIT FAILS`

. The [read-only setup audit](https://github.com/a-dev/skills/blob/main/skills/frontend/css-modules-setup/scripts/audit.mjs) reports missing or drifted plumbing, and `--check`

exits non-zero. If static inspection can't prove something safely, it says `not-verifiable`

and gives you a follow-up command. I prefer that to a suspiciously confident green tick.

**I hide loading in conditional cx, serialize data-loading="false", or mirror disabled into data-disabled.**

`CSS:CHECK FAILS`

. The [ESLint rules](https://github.com/a-dev/skills/blob/main/skills/frontend/css-modules-setup/harness/eslint-plugin.mjs) catch the TSX side. The [Oxlint adapter](https://github.com/a-dev/skills/blob/main/skills/frontend/css-modules-setup/harness/oxlint-plugin.mjs) reports the same rule IDs in a faster, separate loop. Wrong ARIA value selectors and private boolean selectors fail through the [Stylelint rules](https://github.com/a-dev/skills/blob/main/skills/frontend/css-modules-setup/harness/stylelint-plugin.mjs).

*I use a primitive palette token, a raw hex color, or a local theme selector inside a component module.*

`AUDIT OR CSS:CHECK FAILS`

, but only if the project adopted the color-token contract. The audit catches palette leakage; Stylelint handles the per-edit rules.

*I compute styles[kind], use camelCase in CSS, write a bare descendant element selector, keep an ordinary visual inline style, or reach for !important.*

`CSS:CHECK FAILS`

. ESLint and Stylelint cover those source rules. Whether `meta`

is a good role name is still `REVIEW`

. A linter can't tell what the component means.

*I put a module in the wrong cascade layer.*

`CSS:CHECK FAILS`

when the project profile gives that file one unambiguous owner. If ownership itself is unclear, the result is `REVIEW`

, not a layer name invented by the checker.

*I add a class to the shared module or break an external composes path.*

`CSS:CHECK FAILS`

. The [cross-file checker](https://github.com/a-dev/skills/blob/main/skills/frontend/css-modules-setup/scripts/check.mjs) verifies entry-point exports, semantic tokens, and aliased composition paths. When the profile records public classes, the checker also catches drift between that list and the shared module. But whether that class deserves to become shared is `REVIEW`

. The [continuous-work skill](https://github.com/a-dev/skills/blob/main/skills/frontend/css-modules/SKILL.md) follows the project's admission rule; it doesn't invent the answer.

*I use 9px, invent size-xl, or ignore a spacing scale that doesn't exist.*

`PROJECT DECIDES`

. The generic harness stays silent. A project-specific check may fail if the project has one, but this methodology doesn't define or enforce that scale. The [mechanical-enforcement guide](https://github.com/a-dev/skills/blob/main/skills/frontend/css-modules-setup/references/mechanical-enforcement.md) explains the aggregate command, severity, and exception format.

Of course, these days you also need a good harness for the AI that will work with this system. I made [two skills](https://github.com/a-dev/skills/tree/main/skills/frontend); you can install them with `npx skills add a-dev/skills --skill css-modules-setup --skill css-modules`

. Choose a project or global installation, select your host (`codex`

or `claude-code`

), then verify both skills in the host's catalog. The first one, `css-modules-setup`

, is for setting up this whole system, including the `vite-css-modules`

library, the Vite config, and the path alias. It's meant to be invoked explicitly, since setup happens once; how manual commands behave depends on the host.

The second one, `css-modules`

, works during everyday coding after the project adopts the methodology (the model can invoke it on its own). The canonical versions live in the skills repository; the older copy beside this article is only kept for history. If you want to get to the bottom of this methodology, I recommend reading these skills: they're really well documented and have examples.

Think of these skills as a starting point. In recent months I've often found myself adapting different skills to specific projects, and I believe that's the right way to harness AI: tune them to your needs, architecture, and paths. A detailed harness for your project always beats a general one, with better results and fewer hallucinations.

At this point in the article, I asked AI to find objections that I hadn't noticed in the system and to give me the weaknesses without sugar-coating them. We argued for a while, and I made a list of the main conclusions.

**"Generated .d.ts files are a codegen loop. Tailwind doesn't need one."**

`css:generate`

before `css:types`

. That's the whole tax; the payoff is compile-time class safety. And an agent handles this loop happily: it regenerates after changes, runs the checks, and always knows the current class names.**"The variant maps are boilerplate. cva solved this."**

`cva`

happily accepts CSS Modules classes, so if your team loves `cva`

, the map just moves inside it.**"Atomic CSS bundles are smaller."**

At scale, yes, conceded. Gzip can shrink the repeated bytes, but it doesn't solve parse cost, CSSOM memory, or unused CSS. My claims in this article are about readability, debugging and harness. If payload is what matters in your application, measure it there. You can still use shared single-purpose classes with `cx(l.padM, t.h1)`

. The defaults just point in a different direction, and I don't promise identical output.

**"Kebab-case in CSS, camelCase in TSX: you split grep in two."**

Also true: searching `variant-primary`

finds the CSS, `variantPrimary`

finds the usage. Two greps. But try to find `styles[` button--${variant}`]`

, ah?

Declaration maps soften navigation in the editor, where a typed usage jumps straight to the CSS source. How readable the runtime class names are depends on the build mode and configuration.

**"It's all discipline. Nothing forces your rules."**

Half true. Some objective parts can be checked, but a rule is only enforced when a failing check exists. The rest is audited or reviewed. The complaint test above makes that boundary explicit.

**" composes is not standard CSS."**

`cx`

does the same job.**"Who decides what is shared and which layers exist?"**

The project does. The modules and layer list in this article are defaults to start from; the methodology doesn't legislate them. The one stable requirement is that the chosen boundaries and override order get written down and used consistently.

**"Does leaving spacing and sizing open weaken the design system?"**

No, because that is intentionally a project design-system concern. A project can enforce its own scale or allow fluid and contextual values; this methodology doesn't choose for it.

**"What about monorepos, multiple Vite apps, Storybook, SSR, tests, and theme hydration?"**

The methodology alone doesn't solve this. Each environment still has to verify its own aliases, generated declarations, layer entry point, and theme bootstrap. The generic harness defines what correct means; the project's checks have to prove it.

And if you're happy with Tailwind, keep it; this article isn't trying to convert you. Steal the parts that work anywhere: the state-channel convention, cascade layers, and the two-tier design tokens fit any styling approach, even a utility-first one.

That's it for now. The methodology is young, and I expect it to evolve. I hope this article gives you a good overview of the system and its principles. Please share your opinions and objections: a methodology this young needs arguments more than praise. Questions are welcome too.
