Experience System

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:

CategoryRulesIncluded by defaultPurpose
Migration2YesMoves consumers away from legacy packages and unsupported customization.
Composition22YesEnforces component structure, semantics, and locally visible accessibility contracts.
Style8NoApplies 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-system

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

Use the recommended preset to report migration and composition findings as warnings:

eslint.config.js
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:

eslint.config.js
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:

Legacy config

For applications that still use .eslintrc.*, register the @by/experience-system plugin namespace and extend one core preset.

.eslintrc.json
{
  "plugins": ["@by/experience-system"],
  "extends": ["plugin:@by/experience-system/recommended"]
}

Strict

.eslintrc.json
{
  "plugins": ["@by/experience-system"],
  "extends": ["plugin:@by/experience-system/strict"]
}

Add style rules separately

Preset reference

FormatPresetCategoriesSeverity
Flatflat/recommendedMigration and compositionwarn
Flatflat/strictMigration and compositionerror
Flatflat/style-recommendedStylewarn
Flatflat/style-strictStyleerror
LegacyrecommendedMigration and compositionwarn
LegacystrictMigration and compositionerror
Legacystyle-recommendedStylewarn
Legacystyle-strictStyleerror

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.

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.

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:

eslint.config.js
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

.eslintrc.json
{
  "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.
  • 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.