@servicenow/eslint-plugin-aiux-style
v0.4.0
Published
ESLint rules for AIUX styling conventions — promote Tailwind/DaisyUI utilities and design tokens over inline styles and hardcoded colors. Static-analysis companion to the runtime style audit panel.
Readme
@servicenow/eslint-plugin-aiux-style
ESLint rules that keep ServiceNow scoped apps built on Karuna using DaisyUI + Tailwind correctly — promoting aiux--prefixed DaisyUI component classes, Tailwind utilities, and design tokens over inline styles, hardcoded colors, and inaccessible DaisyUI "hacks". Static-analysis companion to the runtime style audit panel (components/audit-panel).
All rules:
- Report at the precise source location of the offending token/declaration (not the template's opening backtick).
- See through
${…}interpolations — a class token or attribute that spans an interpolation (class="btn ${active ? 'on' : ''}") is still analyzed.
Install
pnpm add -D @servicenow/eslint-plugin-aiux-styleUsage
import aiuxStyle from '@servicenow/eslint-plugin-aiux-style';
export default [...aiuxStyle.recommended];Opt into higher-signal, Tailwind-forward guidance with strict:
export default [...aiuxStyle.strict];Or build your own from the rules export:
export default [
{
plugins: {'@servicenow/aiux-style': {rules: aiuxStyle.rules}},
rules: {
'@servicenow/aiux-style/no-inline-styles': 'warn',
'@servicenow/aiux-style/no-unsupported-aiux-daisy-class': 'error'
}
}
];Configs
recommended
| Rule | Severity | What it catches |
| --------------------------------- | -------- | ------------------------------------------------------------------------ |
| no-unsupported-aiux-daisy-class | error | aiux- DaisyUI classes Karuna doesn't safelist (won't render) |
| no-daisyui-tooltip-hack | error | CSS-only data-tip tooltips (inaccessible) |
| no-collapse-input-hack | error | hidden checkbox/radio collapse/accordion (inaccessible) |
| require-aiux-daisy-prefix | warn | unprefixed DaisyUI component classes |
| no-inline-styles | warn | style="…" attribute literals |
| no-hardcoded-colors | warn | hex / rgb() / hsl() / oklch() colors and Tailwind arbitrary colors |
| no-semantic-color-opacity | warn | opacity modifiers on semantic colors (bg-info/10) |
| no-filter-input-hack | warn | DaisyUI aiux-filter component (inaccessible, unsafelisted) |
| no-modal-div-hack | warn | <div class="aiux-modal"> instead of <dialog> / <aiux-modal> |
| prefer-disabled-attr | warn | aiux-btn-disabled class without a real disabled state |
| prefer-skeleton-loader | warn | content loading spinners (use aiux-skeleton) |
strict
Everything in recommended, plus heuristic "you hand-rolled a component" rules (all warn):
| Rule | What it catches |
| ------------------------------ | --------------------------------------------------------- |
| prefer-aiux-button | custom CSS button styling → aiux-btn |
| prefer-aiux-form-control | custom input/textarea/select CSS → aiux-input/… |
| prefer-aiux-card | custom card/panel/tile CSS → aiux-card |
| prefer-aiux-badge | custom badge/pill/tag/chip CSS → aiux-badge/aiux-pill |
| prefer-aiux-tabs | custom tablist/tab CSS → aiux-tabs/aiux-tab |
| no-local-design-token-family | local/legacy token families (--sn-*, --now-*) |
The DaisyUI class contract (generated)
require-aiux-daisy-prefix and no-unsupported-aiux-daisy-class are backed by rules/daisy-safelist.generated.js — the exact set of aiux-* classes Karuna's prebuilt bundle emits, generated from components/core/src/styles/tailwind.css by scripts/generate-daisy-contract.mjs and guarded by test/daisy-safelist.invariant.test.js. Because "supported" is read straight from the safelist, the contract can never contradict ground truth.
Regenerate after changing the Karuna safelist:
pnpm --filter @servicenow/eslint-plugin-aiux-style generateRules
no-inline-styles (warn)
Flags style="…" literals in html`…` templates. CSS-custom-property-only values (style="--x: 12px") and fully dynamic bindings (style=${styleMap(...)}, style="${expr}") are allowed; an interpolated value with static CSS (style="color: red; ${x}") is flagged. Per-element opt-out via data-style-skip (configurable). Options: {allowPattern, optOutAttr}.
no-hardcoded-colors (warn)
Flags hardcoded colors in color-bearing properties: hex / rgb() / hsl() / oklch() / etc. in inline styles and css`…` blocks, plus Tailwind arbitrary-value color classes (text-[#abc], bg-[rgb(0,0,0)]). css blocks are parsed at declaration boundaries, so snippets are clean and never spill across a rule. Allows currentColor/transparent/var(--token). Option: {optOutAttr} (default data-color-skip).
require-aiux-daisy-prefix (warn)
Karuna configures DaisyUI with prefix: 'aiux-', so DaisyUI component classes must use the aiux- form. Tailwind utilities and custom classes stay unprefixed. A token the component styles itself (a .<token> rule in its own css block) is treated as a custom class and suppressed.
// ✗ html`<button class="btn btn-primary">Save</button>`
// ✓ html`<button class="aiux-btn aiux-btn-primary">Save</button>`
// ✓ html`<div class="flex gap-2 pod-card">…</div>` // utilities + custom
// ✓ css`.card { … }` + html`<div class="card">…</div>` // self-styled custom classno-unsupported-aiux-daisy-class (error)
Flags aiux- classes whose root is a real DaisyUI component but which Karuna does not safelist — they silently won't render. Legacy/renamed classes get a targeted fix-it message.
// ✗ html`<input class="aiux-input aiux-input-bordered" />` // v5: bordered by default
// ✗ html`<button class="aiux-btn-glass">Save</button>` // removed in v5
// ✗ html`<nav class="aiux-navbar">…</nav>` // not safelisted in Karuna
// ✓ html`<table class="aiux-table aiux-table-zebra">…</table>` // safelisted → fineno-daisyui-tooltip-hack (error)
Flags the CSS-only data-tip tooltip (no keyboard/focus/SR support), in static, interpolated (data-tip=${label}), and templated forms. Use the tooltip directive/controller. Bare boolean data-tip (which DaisyUI never renders) is not flagged. Skipped in *.stories.js.
no-collapse-input-hack (error)
Flags hidden <input type="checkbox|radio"> inside an aiux-collapse element (announced as "checkbox"/"radio" instead of a disclosure). Detection scopes to the collapse element's actual content, so intervening elements and either ordering are caught. Use native <details>/<summary> (or <aiux-accordion-group>).
no-filter-input-hack (warn)
Flags the DaisyUI filter component (aiux-filter), whose hidden radio/checkbox children are inaccessible (and which Karuna doesn't safelist). The class is the filter root wherever it appears (typically the wrapping <form>). Use <button class="aiux-pill" aria-pressed>.
no-modal-div-hack (warn)
Flags <div class="aiux-modal"> (no focus trapping / Escape / aria-modal). Use <aiux-modal> or native <dialog class="aiux-modal …">. Skipped in *.stories.js. (Class-toggled modals via classMap on a <div> are not detected.)
no-semantic-color-opacity (warn)
Flags opacity modifiers on DaisyUI semantic colors (bg-info/10, text-success/50). Matches whole class tokens, so custom classes like my-bg-primary/50 are not false-flagged; non-semantic utilities (bg-black/50) are allowed.
prefer-disabled-attr (error-type, warn)
Flags aiux-btn-disabled on a <button>/<input> without a real disabled state. Recognizes native disabled, Lit ?disabled=${x} (boolean attr) and .disabled=${x} (property), and aria-disabled="true" for non-interactive display elements.
prefer-skeleton-loader (warn)
Flags content loading spinners (aiux-loading-spinner, …-dots, …) in favor of aiux-skeleton. A spinner inside a button (the spinner's own element or a true ancestor carries aiux-btn) is allowed; a content spinner that merely follows a sibling button is still flagged. Skipped in *.stories.js.
prefer-aiux-button / -form-control / -card / -badge / -tabs (strict, warn)
Heuristic rules that flag high-confidence hand-rolled component CSS in css`…` blocks (a buttonish/inputish/cardish/badgeish/tabish selector carrying the matching cluster of layout/color/shape declarations) and recommend the corresponding Karuna primitive plus Tailwind utilities.
// ✗ static styles = css`.btn-home { background: …; color: …; border-radius: 8px; padding: …; cursor: pointer; }`
// ✓ html`<button class="aiux-btn aiux-btn-primary">Home</button>`no-local-design-token-family (strict, warn)
Flags local/legacy design-token families (--sn-*, --now-*) at both their declaration and use sites, in css blocks and inline style. Prefer Karuna/Horizon tokens or Tailwind/DaisyUI utilities. Option: {prefixes}.
Pairing with the runtime audit panel
The audit panel (components/audit-panel/src/audit-panel.js) walks the live DOM at runtime for the same issues. This plugin catches them earlier — in the editor and on CI — so they never reach a render.
