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.
TLDR
It's real CSS all the way down, just better behaved:
- Atoms you snap together like Lego:
cx(layout.padM, typography.h1). - Cascade layers, so in this setup normal local styles beat shared ones without specificity arm-wrestling or
!important. - Generated types: a typo in a class name breaks the build, not the button.
-
data-*state you can flip right in the Elements panel, instead of clicking through the app to reproduce a bug. - Two-tier color tokens, so "wait, which gray is our gray?" has exactly one answer.
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 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.
What I wanted to keep from Tailwind
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.
Architecture at a glance
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.
Cascade layers: floor and ceiling
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.
Two-tier color tokens
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.
TypeScript for CSS
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, 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.
// 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 runs generation, tsc --noEmit, and a production build.
Core: make atoms in CSS Modules
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:
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:
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.
Class names and data attributes
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:
// 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.
What is actually enforced (harness)
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 runs.
I break the #styles alias or remove declaration generation.
AUDIT FAILS. The read-only setup audit 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 catch the TSX side. The Oxlint adapter reports the same rule IDs in a faster, separate loop. Wrong ARIA value selectors and private boolean selectors fail through the Stylelint rules.
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 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 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 explains the aggregate command, severity, and exception format.
Skills for setup and continuous work
Of course, these days you also need a good harness for the AI that will work with this system. I made two skills; 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.
Objections, or AI explores the methodology
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."
True: this is the system's one moving part, I don't like it, and this is the place where I didn't find an ideal solution and settled for a compromise. But in practice it costs one devDependency and one script: typings are gitignored, the dev server regenerates them on the fly, and CI runs 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."
The map is boilerplate with a job: it's the component's style API, written down and checked for exhaustiveness. And nothing locks you in: 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."
Formally it's ICSS, an implementation detail of CSS Modules. Support is widespread, but external imports and aliases vary by build pipeline, so verify them in yours. The system stands without it: composing in markup with 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.
Top comments (3)
If you want to see it 'in action', I have a public pet project (open-source) a-dev.github.io/quizbun/
The styles, the config rendered by the skill, the lint also rendered by the skill.
It's small project and thats why I can't say that methodology shines here, but can give you an idea of ​​how it all works
I appreciate how you've built upon the concept of atomic CSS from Tailwind, but addressed the issues of readability and maintainability by leveraging CSS Modules. The use of
composesextension and cascade layers to manage style precedence is particularly interesting, as it eliminates the need for specificity arm-wrestling or!important. I'm also intrigued by the idea of generated types and two-tier color tokens, which could significantly reduce errors and inconsistencies in styling. How do you envision this methodology evolving to accommodate more complex, dynamic styling scenarios, such as those involving responsive design or accessibility features?There is no problem with accessibility features or the response design at all, because they are not part of the methodology.
As I see it, the idea is genuinely good, but the harness needs polishing. For now, the config is verbose. I want to make it more concise. Linting can also be improved.