ESLint Plugin
Install and configure Experience System lint rules for composition and style policies.
The @by/eslint-plugin-experience-system package helps applications adopt and use
@by/experience-system consistently. It detects legacy APIs, invalid component compositions,
accessibility problems that are visible in JSX, and opt-in style policies based on static Tailwind
classes.
Use the plugin as an early feedback layer during development and code review. It complements type checking, tests, accessibility audits, and browser validation; it does not replace them.
How the plugin is organized
The plugin exports three rule categories:
| Category | Rules | Included by default | Purpose |
|---|---|---|---|
| Migration | 2 | Yes | Moves consumers away from legacy packages and unsupported customization. |
| Composition | 22 | Yes | Enforces component structure, semantics, and locally visible accessibility contracts. |
| Style | 8 | No | Applies opt-in design and accessibility policies to static Tailwind classes. |
The recommended and strict presets include migration and composition rules. Style rules remain separate so teams can adopt them independently.
The package name and ESLint namespace are intentionally different:
- Install
@by/eslint-plugin-experience-system. - Configure presets and rules under
@by/experience-system.
All 32 rules currently have no configurable options. Presets control severity: recommended presets
use warn, while strict presets use error.
Analysis scope
Composition rules recognize named imports from @by/experience-system and inspect JSX in the
current file. Dynamic prop values are handled on a best-effort basis. The rules do not perform
type-aware, rendered-DOM, runtime, or cross-file analysis, so an ancestor rendered by another
component cannot be observed.
Style rules inspect string literals, static template literals, and statically visible classes in
cn(), clsx(), and cva() calls. Computed classes, CSS files, inline style objects,
inherited styles, runtime contrast, and final rendered dimensions are outside their scope.
Color-step rules are accessibility guardrails, not proof of WCAG contrast conformance. Actual contrast depends on the resolved foreground, background, theme, state, and text size.
Installation
The plugin supports ESLint 8.57 or newer in the 8.x line, and ESLint 9. Install it as a development dependency:
pnpm add --save-dev @by/eslint-plugin-experience-systemFlat config
Flat config is the current ESLint configuration format. Import the plugin in
eslint.config.js and add one core preset.
ESLint 9 uses flat config by default. With ESLint 8, opt in by setting
ESLINT_USE_FLAT_CONFIG=true when running ESLint.
Recommended
Use the recommended preset to report migration and composition findings as warnings:
import experienceSystem from '@by/eslint-plugin-experience-system';
export default [experienceSystem.configs['flat/recommended']];Strict
Use the strict preset to report the same rules as errors:
import experienceSystem from '@by/eslint-plugin-experience-system';
export default [experienceSystem.configs['flat/strict']];Add style rules separately
Add the style preset as a separate array entry. Match its severity to the core preset:
import experienceSystem from '@by/eslint-plugin-experience-system';
export default [
experienceSystem.configs['flat/recommended'],
experienceSystem.configs['flat/style-recommended'],
];Legacy config
For applications that still use .eslintrc.*, register the
@by/experience-system plugin namespace and extend one core preset.
Recommended
{
"plugins": ["@by/experience-system"],
"extends": ["plugin:@by/experience-system/recommended"]
}Strict
{
"plugins": ["@by/experience-system"],
"extends": ["plugin:@by/experience-system/strict"]
}Add style rules separately
{
"plugins": ["@by/experience-system"],
"extends": [
"plugin:@by/experience-system/recommended",
"plugin:@by/experience-system/style-recommended"
]
}Preset reference
| Format | Preset | Categories | Severity |
|---|---|---|---|
| Flat | flat/recommended | Migration and composition | warn |
| Flat | flat/strict | Migration and composition | error |
| Flat | flat/style-recommended | Style | warn |
| Flat | flat/style-strict | Style | error |
| Legacy | recommended | Migration and composition | warn |
| Legacy | strict | Migration and composition | error |
| Legacy | style-recommended | Style | warn |
| Legacy | style-strict | Style | error |
Composition rules
Composition rules inspect relationships among named Experience System components in the current file. When a component boundary hides part of the tree, use a narrow suppression only after confirming that the rendered composition satisfies the policy.
Avoid free-floating actions in display surfaces
Rule: @by/experience-system/avoid-free-floating-actions-in-display-surfaces
Avoid placing actions directly in data-display or feedback surfaces without the expected action container. Use the surface's supported action region so controls remain consistently positioned and understood.
Avoid actions inside actions
Rule: @by/experience-system/avoid-action-inside-action
Do not nest an action component inside another action component. Nested actions produce ambiguous interaction and keyboard semantics; render them as sibling controls instead.
Avoid form controls inside actions
Rule: @by/experience-system/avoid-form-control-inside-action
Do not place form controls inside action components. Separate the control from the surrounding button or link so each interactive element has an independent target and accessible role.
Avoid navigation inside actions
Rule: @by/experience-system/avoid-navigation-inside-action
Do not place navigation components inside action components. Use one element with the correct navigation or action semantic instead of nesting competing interactions.
Keep providers at application boundaries
Rule: @by/experience-system/keep-providers-at-application-boundaries
Mount provider components at application, route, story, or test boundaries rather than inside Experience System UI components. Deliberate provider islands are valid when they represent a real boundary.
Provide form structure for controls
Rule: @by/experience-system/require-form-structure-ancestor-for-form-controls
Wrap form controls in a supported Field, FieldGroup, FieldSet, InputGroup, or
Label structure. Analysis is file-scoped and cannot detect an ancestor supplied in another
file.
Keep control state props off labels
Rule: @by/experience-system/avoid-control-state-props-on-label
Do not put control-state props such as aria-invalid or disabled on Label. Apply
them to the associated form control so assistive technology receives the state on the interactive
element.
Give dialogs a title
Rule: @by/experience-system/require-dialog-title-in-dialog-content
Include a DialogTitle descendant in DialogContent so the dialog has an accessible name.
The title may be visually hidden when the design does not call for a visible heading.
Avoid nested dialogs
Rule: @by/experience-system/avoid-nested-dialogs
Do not nest one dialog inside another. Prefer a single dialog flow or close the current dialog before opening the next so focus and dismissal behavior remain predictable.
Keep floating actions out of Cards and Dialogs
Rule: @by/experience-system/avoid-fab-inside-card-or-dialog
Do not place Fab inside Card or Dialog. Use the container's normal action layout;
floating actions are intended for page-level emphasis.
Limit dialogs to one primary action
Rule: @by/experience-system/limit-primary-actions-in-dialog
A dialog should expose no more than one action using variant="fill". Give the primary outcome
clear emphasis and render secondary actions with less prominent variants.
Use a Button or Link as a Popover trigger
Rule: @by/experience-system/require-button-or-link-trigger-for-popover
The Popover trigger must resolve to a Button or Link. This gives the trigger an expected
interactive semantic and keyboard behavior.
Label icon-only controls
Rule: @by/experience-system/require-aria-label-for-icon-only-button
Icon-only Button and Toggle controls require a non-empty aria-label. The label must
describe the action rather than the icon's appearance.
Name tables that do not have a title
Rule: @by/experience-system/require-accessible-name-for-experience-system-table-without-title
An ExperienceSystemTable without a title must provide aria-label or
aria-labelledby. Use a concise name that identifies the table's data or purpose.
Give toolbars accessible names
Rule: @by/experience-system/require-accessible-name-for-toolbar
Every Toolbar and ToolbarToggleGroup must provide aria-label or
aria-labelledby, allowing assistive technology users to distinguish the group of controls.
Include an Accordion trigger
Rule: @by/experience-system/require-accordion-trigger-descendant
An Accordion item should include an AccordionTrigger descendant to expose the control that
expands its content. The relationship must be visible in the current file.
Limit interactive content in Accordion triggers
Rule: @by/experience-system/limit-interactive-children-in-accordion-trigger
Do not put additional interactive content inside AccordionTrigger. Nested interactions can
compete with the trigger's click and keyboard handling; place related controls beside it.
Keep Breadcrumb links inside Breadcrumb
Rule: @by/experience-system/require-breadcrumb-ancestor-for-breadcrumb-link
Use BreadcrumbLink only inside Breadcrumb. Analysis is file-scoped and cannot detect a
Breadcrumb ancestor supplied in another file.
Keep Breadcrumb separators non-interactive
Rule: @by/experience-system/keep-breadcrumb-separator-non-interactive
Do not make BreadcrumbSeparator interactive. Separators communicate hierarchy visually and
should not become navigation or action targets.
Put Empty title before description
Rule: @by/experience-system/require-empty-title-before-empty-description
When both are present, render EmptyTitle before EmptyDescription. The order preserves the
expected heading-first reading and announcement sequence.
Avoid nested Scroll Areas
Rule: @by/experience-system/avoid-nested-scroll-area
Do not place ScrollArea inside another ScrollArea. Use one scrolling boundary or
restructure the layout to avoid competing scroll containers.
Keep interactive content out of tooltips
Rule: @by/experience-system/avoid-interactive-content-inside-tooltip-content
Do not put interactive Experience System components inside TooltipContent. Tooltips are
supplemental descriptions, not containers for controls; use a Popover or Dialog for interaction.
Migration rules
Migration rules identify legacy package usage and customization patterns that do not carry forward to the Experience System.
Avoid inline styling on legacy components
Rule: @by/experience-system/no-inline-style-class-in-by-es
Avoid inline style and class customization on legacy BY component packages. Such customization can depend on implementation details and may not transfer to the standard Experience System design.
Prefer newer Experience System components
Rule: @by/experience-system/prefer-by-es-newer-components
Prefer components exported by @by/experience-system over equivalent exports from legacy
packages. The finding points consumers toward the supported replacement API.
Style rules
Style rules are opt-in. They inspect statically visible Tailwind classes and enforce design or accessibility policies without trying to calculate the final rendered style.
Avoid uppercase UI labels
Rule: @by/experience-system/no-uppercase-ui-labels
Disallows uppercase on native UI labels and Experience System action or label components.
Use sentence case in the source text instead of applying an all-uppercase transformation.
Keep prose start-aligned
Rule: @by/experience-system/no-centered-or-justified-prose
Disallows centered or justified paragraph text. Keep prose left-aligned or start-aligned for readability and bidirectional layout support.
Use approved text color steps
Rule: @by/experience-system/no-restricted-text-color-steps
Reports text color steps 4 through 10 because they are not approved foreground tokens. Choose a supported semantic foreground token and verify contrast in every applicable theme and state.
Keep semantic colors out of prose
Rule: @by/experience-system/no-semantic-color-on-prose
Prevents error, warning, success, info, and priority color utilities from being used as paragraph typography. Reserve status colors for deliberate semantic indicators rather than body copy.
Prefer the page-grid utility
Rule: @by/experience-system/prefer-grid-page
Suggests grid-page when a class list recreates the complete standard page-grid recipe. The
rule can offer an ESLint suggestion to replace that recipe; see Page grid
for its defaults and override behavior.
Use opaque overlay surfaces
Rule: @by/experience-system/no-translucent-overlay-surface
Disallows translucent background utilities on known overlay content surfaces such as dialogs and menus. Use an opaque surface token so underlying content does not reduce readability.
Use approved color steps
Rule: @by/experience-system/no-restricted-color-steps
Reports non-text color-bearing utilities that use steps 4 through 8, including backgrounds, borders, fills, strokes, rings, gradients, and related utilities. Use an approved semantic or palette step for the surface or decoration.
Use approved Card color steps
Rule: @by/experience-system/no-restricted-card-color-steps
Reports Card color utilities that use steps 4 through 8. Choose an approved Card color token
that preserves the intended surface hierarchy and contrast.
Disable a rule
Prefer fixing the reported composition or style. When a rule does not apply, disable it as narrowly as possible and leave a reason that explains the exception.
Flat config override
Place the override after the preset so it takes precedence:
import experienceSystem from '@by/eslint-plugin-experience-system';
export default [
experienceSystem.configs['flat/recommended'],
{
rules: {
'@by/experience-system/require-breadcrumb-ancestor-for-breadcrumb-link': 'off',
},
},
];Legacy config override
{
"plugins": ["@by/experience-system"],
"extends": ["plugin:@by/experience-system/recommended"],
"rules": {
"@by/experience-system/require-breadcrumb-ancestor-for-breadcrumb-link": "off"
}
}Local suppression
Use a file-level comment only when the exception applies throughout the file:
/* eslint-disable @by/experience-system/require-breadcrumb-ancestor-for-breadcrumb-link -- Breadcrumb is provided by the parent layout. */For a single occurrence, prefer a next-line or same-line suppression:
// eslint-disable-next-line @by/experience-system/require-breadcrumb-ancestor-for-breadcrumb-link -- Breadcrumb is provided by the parent layout.
<BreadcrumbLink href="/orders">Orders</BreadcrumbLink>;
<BreadcrumbLink href="/orders">Orders</BreadcrumbLink>; // eslint-disable-line @by/experience-system/require-breadcrumb-ancestor-for-breadcrumb-link -- Breadcrumb is provided by the parent layout.Related guidance
- Accessibility explains the broader accessibility practices that syntax-level lint rules complement.
- Theming documents the tokens and theme boundaries referenced by Experience System styling policies.
- Page grid explains the utility recommended by
prefer-grid-page. - Migration covers the wider application migration strategy.
The package source and developer README live under
packages/eslint-plugin-experience-system in the Experience System repository.