@airframeui/eslint-plugin
v0.5.0
Published
Airframe UI ESLint plugin — catch Tailwind/Bootstrap drift, unknown af-* classes, and redundant defaults in JSX and Angular templates
Maintainers
Readme
@airframeui/eslint-plugin
ESLint rules that keep JSX and Angular templates on Airframe: leftover Tailwind/Bootstrap utilities, unknown af-* names, redundant defaults, structure classes, and field-error ARIA.
String literals on class / className in .js / .jsx / .ts / .tsx, plus Angular class="…", [class]="'…'", [class.token], and [ngClass] string/map keys in .html (and inline template: via angular-eslint processInlineTemplates). It does not parse Vue bindings or class:list. For those hosts, use MCP validate_markup (or pretty-print @airframeui/core/catalog when MCP is off).
Same version as @airframeui/core. Docs: airframeui.com/docs/eslint-plugin · Changelog
Installation
npm install -D @airframeui/eslint-pluginRequires ESLint 9+ (flat config) or ESLint 8.57+ with eslintrc. Core and tokens are optional peers — if they are installed, their catalogs win so lint matches the CSS you ship.
Angular templates need @angular-eslint/template-parser (Angular 20+, typically via angular-eslint) on **/*.html. The plugin does not parse HTML as JavaScript.
Setup
Flat config (ESLint 9+)
// eslint.config.js
import airframe from '@airframeui/eslint-plugin';
export default [airframe.configs['flat/recommended']];Drop airframe.configs['flat/recommended'] into an existing export default [ ... ] array. It enables JSX parsing and matches .js, .jsx, .ts, and .tsx (ESLint does not lint .jsx / .ts unless files says so).
Angular templates (v20+)
Use alongside angular-eslint. flat/angular turns the Airframe rules on for **/*.html; it does not set a parser — templateRecommended (or languageOptions.parser: angular.templateParser) must already apply.
// eslint.config.js
import airframe from '@airframeui/eslint-plugin';
import angular from 'angular-eslint';
export default [
...airframe.configs['flat/recommended'],
{
files: ['**/*.ts'],
processor: angular.processInlineTemplates,
},
...angular.configs.templateRecommended,
...airframe.configs['flat/angular'],
];processInlineTemplates extracts @Component({ template: \…` })so the same rules run on inline templates. External*.htmlfiles are covered byflat/angular`.
Override a rule after the preset:
export default [
airframe.configs['flat/recommended'],
{
rules: {
'@airframeui/no-tailwind-classes': 'off',
},
},
];Legacy eslintrc
// .eslintrc.cjs
module.exports = {
plugins: ['@airframeui'],
extends: ['plugin:@airframeui/recommended'],
};Rules
All seven rules ship in recommended (JSX) and flat/angular (templates). They inspect string literals on class / className in JSX, and Angular class, [class]="'…'", [class.token], and [ngClass] string/map keys. Existing af-* tokens are ignored by the Tailwind and Bootstrap detectors.
| Rule | Recommended | Autofix | What it does |
| -------------------------------------------- | ----------- | ------- | -------------------------------------------------------------------------------------------------------------------- |
| @airframeui/no-bootstrap-classes | warn | no | Leftover Bootstrap tokens (btn, row, col-*, d-flex, …) |
| @airframeui/no-redundant-defaults | warn | yes | Restated defaults: af-card af-stack, af-btn af-is-primary, af-container af-container-xl, matching af-text-h* |
| @airframeui/no-redundant-width | warn | yes | af-w-full on form controls; af-block on af-btn |
| @airframeui/no-tailwind-classes | warn | no | Leftover Tailwind utilities (flex, gap-4, md:gap-4, bg-blue-600, …) |
| @airframeui/no-unknown-airframe-class | error | yes* | Invented af-* names. Autofix for known aliases and catalog heuristics |
| @airframeui/prefer-airframe-layout-recipes | warn | yes | Rewrites flex flex-col, grid grid-cols-*, row, container, … |
| @airframeui/require-field-error-aria | error | no | af-field__error needs id; error inputs need aria-describedby |
Autofix
Rules with a fix rewrite the class / className string (quotes stay; an empty class attribute is removed). The same rewrite applies to static class="…" in Angular templates and to [class]="'…'" string literals. [class.token] and [ngClass] object maps are reported without a rewrite.
npx eslint . --fixSame command as the editor Fix all auto-fixable problems action.
Autofix only runs when the replacement is deterministic: known aliases, catalog heuristics, structure classes, redundant defaults, and redundant width. Unknown names with only a fuzzy guess, leftover Tailwind/Bootstrap that is not a structure rewrite, and field-error ARIA are reported without a fix.
Rewrites
Structure classes
@airframeui/prefer-airframe-layout-recipes — skipped if the attribute already has af-stack, af-inline, af-grid, af-cluster, af-spread, af-center, or af-container.
Gap numbers map to the Airframe named scale (0 · xs · sm · md · lg · xl · 2xl · 3xl · 4xl). Default md (gap-3 / g-3) is omitted because af-stack / af-grid already use it.
| Before | After |
| ----------------------------------------- | ----------------------------------------- |
| flex flex-col / flex flex-col gap-4 | af-stack / af-stack af-gap-lg |
| flex flex-wrap / flex flex-wrap gap-2 | af-inline / af-inline af-gap-sm |
| grid / grid grid-cols-3 gap-6 | af-grid / af-grid af-grid-3 af-gap-xl |
| d-flex flex-column | af-stack |
| d-flex flex-wrap | af-inline |
| d-grid / row | af-grid |
| container / container-fluid | af-container |
Unknown af-* names
@airframeui/no-unknown-airframe-class — prefers the project's @airframeui/core/classes.json, then the snapshot bundled at plugin build time.
| Invented | Autofix |
| ----------------------------------------------------------------------------- | -------------------------------------------- |
| af-btn-primary | af-btn |
| af-btn-secondary / af-btn-danger | af-btn af-is-secondary / af-is-danger |
| af-btn-destructive | af-btn af-is-danger |
| af-btn-block / af-is-block / af-is-full | af-w-full |
| af-{pattern}-{variant} when both af-{pattern} and af-is-{variant} exist | af-{pattern} af-is-{variant} |
| Anything else unknown | Message may suggest a close name; no autofix |
Do not invent af-* names. Use catalog classes (af-btn af-is-danger, not af-btn-danger).
Redundant markup
| Before | After | Rule |
| ------------------------------------------ | ---------------------- | ----------------------- |
| af-btn af-is-primary | af-btn | no-redundant-defaults |
| af-card af-stack af-gap-md | af-card | no-redundant-defaults |
| af-grid af-gap-md / af-stack af-gap-md | af-grid / af-stack | no-redundant-defaults |
| af-container af-container-xl | af-container-xl | no-redundant-defaults |
| <h1 class="af-text-h1"> | <h1> | no-redundant-defaults |
| af-input af-w-full | af-input | no-redundant-width |
| af-btn af-block | af-btn af-w-full | no-redundant-width |
Kept: af-card af-gap-lg, af-card af-inline, af-container af-container-lg@xl, <h2 class="af-text-h1">, af-btn af-w-full, af-input af-is-autosize af-w-full.
Form controls that already span full width: af-input, af-textarea, af-select, af-range, af-progress, af-meter, af-input-group, af-field__control.
Field-error ARIA
No autofix — you must choose the id.
| Markup | Required |
| -------------------------------------------------------- | --------------------------------------------- |
| class contains af-field__error | id on that element (for aria-describedby) |
| class contains af-is-error, or aria-invalid="true" | aria-describedby on that element |
<label className="af-field">
<span className="af-field__label">Email</span>
<input className="af-input af-is-error" aria-describedby="email-error" />
<p className="af-field__error" id="email-error">
Enter a valid email.
</p>
</label>For coding agents
Enable recommended, then treat lint as the source of truth for af-* markup:
write class / className (and Angular class / [class] / [ngClass]) → npx eslint . --fix → fix remaining reports by hand- Apply
--fixfirst. Do not invent a differentaf-*name when the message already suggests one. - Map leftover Tailwind/Bootstrap layout combos with the tables above. Do not leave
flex flex-colnext toaf-stack. - For field-error ARIA, add a real
idand matchingaria-describedby. There is no autofix. - Do not wrap classes in template literals,
clsx, orclass:listto dodge the rules — those forms are not checked yet, and the markup is still wrong.
Current limitations
- JSX and Angular templates. String literals on
class/classNamein.js/.jsx/.ts/.tsx. Angular*.html(v20+ control flow included) with@angular-eslint/template-parser:class="…",[class]="'…'",[class.token],[ngClass]strings and map keys. Not Vue/Svelte bindings,class:list, template literals, Astro expression attributes, or[class]="variable". For other HTML hosts, use MCPvalidate_markup. @airframeui/no-unknown-airframe-classprefers the project's@airframeui/core/classes.json, then the snapshot generated from core at plugin build time (sameclasses.json+ token catalog as IntelliSense for Airframe).- Tailwind/Bootstrap detectors ignore existing
af-*tokens soaf-grid/af-btnare not flagged.
Related
- Docs: airframeui.com/docs/eslint-plugin
- Angular setup —
examples/angularin the repo (pnpm example:angular) - Changelog
@airframeui/core— CSS system (install this in the app)- IntelliSense for Airframe — autocomplete for
af-*/--af-* - Coming from Bootstrap or Tailwind
- Packages
License
MIT
