Nagi CSS
CSS, after the wind.
A structural contract for component CSS: reuse applicable definitions, make unregistered meanings visible, and use STN for structure. Lint and measurement share the same analysis of names and owned CSS paths.
Nagi (“NAH-ghee”) is Japanese for the calm after the wind. In a component, that calm begins when naming, structure, and ownership stop drifting apart.
Let naming settle
Nagi CSS connects four ideas: Surface ownership, a base role, static variants and attribute state, and CSS that follows owned DOM. Internal roles are Predefined, Defined, Unregistered, or Structural. HTML and explicit ARIA produce Predefined roles; Built-in Anatomy and Custom definitions validate author-selected meanings.
-
Derived, not remembered
File names and applicable platform mappings supply mechanical names. Built-in Anatomy and Custom definitions make author-selected meanings reusable, with unregistered names visible as warnings.
-
Structure remains visible
Owned selector paths follow the component tree. When markup moves, the corresponding CSS path moves with it—or the contract speaks.
-
Ownership has an edge
A component may place another owned surface, but it does not reach through that surface and style the child’s private DOM.
One base, with an explicit basis
Use a Predefined name when platform evidence fixes it, select a Built-in or Custom Defined name where it applies, use an Unregistered semantic name where needed, or choose STN for structure. Surface roots remain separate.
<article class="invoice-card">
<header class="header">
<div class="media">
<img class="image" src="acme.svg" alt="Acme Ltd.">
</div>
<h3 class="title">Invoice</h3>
</header>
<div class="unit">
<dl class="list -description">
<div class="field -user">
<dt class="term">User</dt>
<dd class="definition">A. Lovelace</dd>
</div>
<div class="field -amount">
<dt class="term">Amount</dt>
<dd class="definition">
<span class="value" data-status="paid">¥12,000</span>
</dd>
</div>
</dl>
<div class="seg">
<small class="note">Paid July 1, 2026.</small>
</div>
</div>
<footer class="footer">
<button class="button -secondary">Download</button>
<button class="button -primary" disabled>
Send
</button>
</footer>
</article>
-
Surface — from the file name
invoice-cardis the component file's name, kebab-cased. The author chooses the responsibility; deriving its class is mechanical. -
Predefined roles — HTML and ARIA
Common HTML elements map to one fixed base role: headings become
title,imgbecomesimage,dtbecomesterm, andddbecomesdefinition. These names are looked up, not invented. -
Built-in and custom meanings
Nagi includes built-in names such as
textandicon. You can also define your own component-scoped vocabulary. Roles name bases; purposes name variants. How to define roles and purposes → -
Structural Tier Names (STN)
A structural ladder (
unit·seg·fr·g) for thedivs where the author chooses structural naming. Shared sibling tiers need no variants. Structural names are valid choices, not a score to minimize. -
Variants — modifiers, not names
-user,-primary, … — they qualify an anchor, never name what an element is, and stay alphabetical. -
State — attributes, not classes
disabled,aria-*,data-*— runtime state never becomes a class.
A checked example, kept executable
Nagi CSS is for teams that keep CSS beside component markup and want names, DOM paths, and ownership boundaries to remain reviewable as that markup changes. It is an ESLint contract for Vue, Nuxt, Svelte, and Astro—not a UI library, runtime, or required dependency on Nagi UI.
This checked Vue fixture derives its root from
profile-card.vue, uses fixed element roles,
and stops at the owned UserAvatar boundary.
<article class="app-profile-card">
<header class="header">
<h2 class="title">Profile</h2>
</header>
<div class="unit">
<span class="text">Ada Lovelace</span>
</div>
<user-avatar />
</article>
.app-profile-card {
> .header { > .title { margin: 0; } }
> .unit { > .text { color: var(--color-text); } }
> .app-user-avatar { align-self: center; }
}
Result: 0 errors.
-
The base role is guessed
Violation
<article class="app-user-card">Diagnostic
nagi-css/surface-root-name Surface root must be named ".app-profile-card" from the configured prefix and Vue file name.Repair: use
app-profile-card, derived fromprofile-card.vueand the configuredapp-prefix. -
Markup moves but its old CSS path remains
Violation
> .header { > .title { > .text { color: var(--color-text); } } }Diagnostic
nagi-css/selector-mirrors-template Selector "> .text" does not follow the template: no element matches this path.Repair: follow the new owned path through
> .header > .unit > .text. -
A parent reaches into an owned child
Violation
.app-profile-card { > .app-user-avatar { > .image { border-radius: 50%; } } }Diagnostic
nagi-css/owned-surface-reach-in Selector "> .image" reaches below ".app-user-avatar", the root of an owned child component; that DOM belongs to the child's surface, so style it there or pass a value in.Repair: keep only external placement on
> .app-user-avatar; style the image inuser-avatar.vueor expose an intentional input.
Each diagnostic is asserted by an executable fixture using the real
ESLint plugin.
Read all four fixtures
or run vp run test and look for
ESLint validates the ProfileCard example and reports three isolated structural violations.
Bring the contract into a component
Install the ESLint plugin, then add it after the framework's official flat config. The framework keeps ownership of its parser and generated settings.
npm install --save-dev @nagi-labs/eslint-plugin-nagi-css
Using pnpm or Vite+? Use the equivalent development-dependency command for your package manager.
import nagiCss from "@nagi-labs/eslint-plugin-nagi-css"
export default [
// framework config first
...nagiCss.configs.recommended({
surfaceRootPrefixes: ["app-"],
}),
]
One ESLint run validates templates, selectors, CSS nesting, ownership, and values. A separate Nagi configuration file is not required for normal Vue, Nuxt, Svelte, or Astro setup.
Choose a guide for Vue, Nuxt, Svelte, or Astro.
For a complete isolated Vite project pinned to Node 22.18+, ESLint 10.10,
Vue 3.5.42, and published npm dependencies, use the
Vue minimal example.
Run vp install --frozen-lockfile,
vp run lint, and vp run build.
Reproduce the role report
The shared analyzer counts styled internal template declarations as Predefined, Defined, Unregistered or Structural. Surfaces, component boundaries, invalid roles and unknowns are separate. Predefined platform names and author-selected Defined names use separate measures.
The checked Vue, Svelte and Astro naming examples each contain one Predefined and three Defined roles: together P=3, D=9, U=0, T=0, N=12. Definition coverage is 12/12 (100.0%); Predefined rate is 3/12 (25.0%) and Defined rate is 9/12 (75.0%). Three Surface roots are excluded. These are reproducibility fixtures, not evidence of a universal rate, human cognitive load or maintenance time.
vp run evaluate:roles
Run from the repository checkout. The 0.7.0 definition and measure APIs are
described in the
migration guide.
Version 0.8 adds optional layer and declaration metadata; see the
0.8 migration guide.
Version 0.9 names purpose target constraints on;
see the 0.9 migration guide.
Inspect the
three-framework examples
and the report specification.
Nagi UI's
separate token-naming evaluation
uses different categories and denominators and must not be presented as this coverage measure.
No STN reduction target or passing threshold is imposed.
A narrow guarantee, with visible tradeoffs
Tailwind puts presentational utilities in markup; StyleX provides typed,
deterministic property composition; Panda generates typed atomic styles
and recipes; CSS Modules provide local class scope. Nagi CSS does not
replace those guarantees. It keeps ordinary component CSS and instead
checks that semantic roles, selectors, and owned DOM paths agree.
Its experimental Tailwind @apply backend is
separate from the stable plain-CSS mode.
It does not prove that the DOM semantics, accessibility, visual result, or maintenance cost are correct. It cannot completely analyze dynamic class names or DOM outside the owned boundary. The question is narrower: when component-owned CSS is your chosen medium, does making these structural decisions executable help your real changes?
Follow a real application from Deep Sea's dashboard source into its lint configuration. Deep Sea also uses Nagi UI, but Nagi CSS itself does not require it.
Built for AI agents
The repository ships an agent skill that packages the contract as a workflow: what to derive, in what order, and how to verify the result. An agent selects meanings using definitions; the linter checks the result. That loop keeps AI-written CSS inside the same rules applied to human-written code.
Thank you, RSCSS
Nagi CSS would not exist in its current form without RSCSS by Rico Sta. Cruz.
RSCSS showed me that a small set of conventions—thinking in components, naming elements locally, and using direct-child selectors to protect component boundaries—could make CSS dramatically easier to reason about.
Nagi CSS takes those ideas in a more mechanically enforceable direction, deriving names from HTML and checking ownership boundaries statically. But its starting point is unmistakably RSCSS.
Thank you, Rico, for publishing an approach that has shaped how I think about CSS for years.