Nagi CSS

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.

The idea

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.

Naming

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-card is 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, img becomes image, dt becomes term, and dd becomes definition. These names are looked up, not invented.

  • Built-in and custom meanings

    Nagi includes built-in names such as text and icon. 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 the divs 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

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.

  1. 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 from profile-card.vue and the configured app- prefix.

  2. 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.

  3. 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 in user-avatar.vue or 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.

Use it

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.

Measurement

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.

Scope

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.

For agents

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.

Acknowledgements

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.