Nagi CSS

Nagi CSS

CSS, after the wind.

A structural contract for component CSS that derives class identities and owned selector paths from source, then checks that they stay aligned. It lets class names follow from the source, rather than gather around it.

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 begins with a bounded canonical form: given the same component source and project configuration, authors and agents derive the same root, element identities, and owned selector paths. The remaining anatomy and variant choices stay inside an explicit, reviewable vocabulary.

Naming

One component, named by rule

Little below needs to be invented. Each base identity follows from a lookup or a bounded structural choice, and the linter keeps that derivation attached to the markup.

<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. Never chosen.

  • Elements — from a fixed table

    Common HTML elements map to one fixed identity: headings become title, img becomes image, dt becomes term, and dd becomes definition. These names are looked up, not invented.

  • UI anatomy — a tiny allowlist

    Names for common UI parts that HTML has no tag for: field (form field or composite control), value, actions, media, icon by default — five words go a long way, and the list is configurable per project.

  • Structural Tier Names (STN)

    A structural ladder (unit · seg · fr · g) for the divs that nothing else fits — the tier is determined by depth, so wrapper and inner never appear.

  • 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 identities, 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 identity 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.

Evaluation

Tested beyond a toy component

To see how the contract behaves beyond a small fixture, we measured Nagi UI, a larger set of Vue components built with Nagi CSS. Its executable evaluation counts every literal class token, then separates names that are determined or bounded from the local words an author still chooses:

The 100% base result and the 17.2% variant result use different populations: 555 base identities versus all 670 explicit class tokens. An occurrence is a use in markup, not a separate invention. The source set includes documentation and preview fixtures, where the variant rate is higher, and excludes package Blueprints. This is one self-hosted repository, not a benchmark or a universal Nagi CSS rate. Read the method and limitations, inspect the JSON result, or reproduce it with vp run evaluate:naming. The command is checked in Nagi UI's CI. These counts locate the remaining naming choices; they do not measure human cognitive load.

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 identities, 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 generates against a deterministic contract; 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.