ESLint

Optional Integration

Our ESLint integration is not required to use the QUI component library.

Plugin

We provide an ESLint plugin that enforces best-practices for accessibility and component consumption. This guide assumes you've already set up and configured ESLint. If not, we recommend starting with the configs section below.

In Development

This plugin is currently in development. New rules will be added over time. Refer to the changelog for updates.

Installation

This plugin is provided in ESM format. You will need "type": "module" in your project's package.json to consume them.

npm i --save-dev @qualcomm-ui/eslint-plugin-react

Setup

Add the plugin to your ESLint configuration:

import {defineConfig} from "eslint/config"
import quiEslintPluginReact from "@qualcomm-ui/eslint-plugin-react"

export default defineConfig([
  // ...the rest of your config
  {
    files: ["**/*.{jsx,tsx}"],
    extends: [quiEslintPluginReact.config],
  },
])

Rules

accessible-name

Enforces that certain QUI components have an aria-label or aria-labelledby attribute for accessibility.

Affected components:

  • IconButton
  • InlineIconButton
  • HeaderBarActionIconButton
// Invalid
<IconButton icon={/*...*/} />

// Valid
<IconButton icon={/*...*/} aria-label="Close dialog" />
<IconButton icon={/*...*/} aria-labelledby="close-label" />

avatar-image-alt

Enforces that Avatar.Image components have an alt attribute for accessibility.

// Invalid - no alt
<Avatar.Root>
  <Avatar.Image src="/user.jpg" />
</Avatar.Root>

// Valid
<Avatar.Root>
  <Avatar.Image alt="John Doe" src="/user.jpg" />
  <Avatar.Content>JD</Avatar.Content>
</Avatar.Root>

input-label-association

Enforces that form input components have proper label association for accessibility. Supports both simple and compound component patterns.

Affected components:

ComponentComposite ElementComposite Props
TextInputInputinputProps
NumberInputInputinputProps
PasswordInputInputinputProps
ComboboxControlinputProps
SelectControlcontrolProps
SwitchHiddenInputhiddenInputProps
CheckboxHiddenInputhiddenInputProps
RadioHiddenInputhiddenInputProps

Simple component usage:

// Invalid - no label
<TextInput placeholder="Enter name" />

// Valid - label prop
<TextInput label="Full name" placeholder="Enter name" />

// Valid - direct aria-label (these are forwarded to the internal input element)
<TextInput aria-label="Full name" placeholder="Enter name" />

// Valid - aria-label in composite props
<TextInput inputProps={{"aria-label": "Full name"}} placeholder="Enter name" />
<Switch hiddenInputProps={{"aria-label": "Enable notifications"}} />

Compound component usage:

// Invalid - no label
<TextInput.Root>
  <TextInput.Input placeholder="Enter name" />
</TextInput.Root>

// Valid - Label child
<TextInput.Root>
  <TextInput.Label>Full name</TextInput.Label>
  <TextInput.Input placeholder="Enter name" />
</TextInput.Root>

// Valid - aria-label on composite element
<Switch.Root>
  <Switch.HiddenInput aria-label="Enable notifications" />
  <Switch.Control />
</Switch.Root>

Configs

We provide shared ESLint configurations to enforce consistent code style and quality across our internal projects. These packages wrap popular open source plugins and provide a common baseline that makes it easier for developers to move between codebases.

Overview

Our configs are provided in ESM format. You will need "type": "module" in your project's package.json to consume them.

Two packages are available for React projects:

  • @qualcomm-ui/eslint-config-typescript - TypeScript rules
  • @qualcomm-ui/eslint-config-react - React-specific rules

Both packages use ESLint's flat config format.

Installation

npm i --save-dev @qualcomm-ui/eslint-config-typescript @qualcomm-ui/eslint-config-react

Skip ahead to the Example Configuration section for a complete setup, or continue reading to learn more about the individual configs.

TypeScript Configs

The @qualcomm-ui/eslint-config-typescript package exports the following configurations:

ConfigDescription
baseCore TypeScript rules
recommendedIncludes all configs below
styleGuideCode style enforcement
sortKeysObject key ordering
typeChecksType-aware linting rules
namingConventionsIdentifier naming patterns
performancePerformance-related rules
strictExportsExport restrictions
jsdocJSDoc comment validation

Style Guide Rules

The styleGuide config enforces these conventions:

  • curly - Require braces for all control statements
  • eqeqeq - Require === and !== (except for null comparisons)
  • @stylistic/spaced-comment - Balanced spacing in comments
  • @typescript-eslint/explicit-member-accessibility - Require explicit visibility modifiers, but omit public
  • comment-length/* - Maximum 85 characters per comment line
  • object-shorthand - Require shorthand syntax for object properties
  • prefer-template - Require template literals over string concatenation
  • prefer-const - Require const for variables that are never reassigned
  • no-var - Disallow var declarations

Import Rules

The style guide enforces import organization with lines between groups:

// 1. Built-in and external packages
import {readFile} from "node:fs/promises"
import React from "react"

// 2. Internal imports
import {utils} from "@/lib/utils"

// 3. Parent imports
import {parent} from "../parent"

// 4. Sibling imports
import {sibling} from "./sibling"

Additional import rules:

  • import/no-duplicates - Merge duplicate imports with inline type specifiers
  • unused-imports/* - Auto-remove unused imports (variables prefixed with _ are ignored)

Private Fields

The no-restricted-syntax rule bans the # private field syntax:

// Disallowed
class Foo {
  #bar = 1
}

// Use TypeScript access modifiers instead
class Foo {
  private bar = 1
}

React Configs

The @qualcomm-ui/eslint-config-react package exports the following configurations:

ConfigDescription
baseCore React rules
recommendedBase config plus hooks and JSX rules
strictRecommended plus React Compiler rules

The recommended config includes:

  • react-hooks/rules-of-hooks - Enforce Rules of Hooks
  • react-hooks/exhaustive-deps - Verify effect dependencies (with autofix)
  • react/jsx-sort-props - Sort JSX props (key and ref first)
  • react/jsx-boolean-value - Omit redundant ={true} for boolean props
  • react/self-closing-comp - Require self-closing tags for components without children

The config also blocks the default React import:

// Disallowed
import React from "react"

// Use named imports
import {useState, useEffect} from "react"

Strict Rules (React Compiler)

The strict config adds React Compiler rules for optimization:

  • react-hooks/purity - Enforce pure render functions
  • react-hooks/immutability - Enforce immutable data patterns
  • react-hooks/use-memo - Validate useMemo usage
  • react-hooks/preserve-manual-memoization - Respect manual memoization
  • react-hooks/static-components - Identify static components

Use the strict config when using the React Compiler:

import tsConfigs from "@qualcomm-ui/eslint-config-typescript"
import reactConfigs from "@qualcomm-ui/eslint-config-react"

export default [
  ...tsConfigs.configs.recommended,
  ...reactConfigs.configs.strict,
]

Type-Aware Rules

The TypeScript configs include type-aware rules that require type information. Files matched by your ESLint config's files patterns must be included in the nearest tsconfig.json.

If ESLint errors with "file not found in any configured project," ensure the file is included in your tsconfig's files or include patterns.

Example Configuration

Type-aware rules

This configuration uses type-aware linting. Learn more here about potential pitfalls.

Create an eslint.config.js file in your project root:

import {defineConfig} from "eslint/config"
import * as tseslint from "typescript-eslint"

import quiEslintReact from "@qualcomm-ui/eslint-config-react"
import quiEslintTs from "@qualcomm-ui/eslint-config-typescript"
import quiEslintPluginReact from "@qualcomm-ui/eslint-plugin-react"

const tsLanguageOptions = {
  parser: tseslint.parser,
  parserOptions: {
    projectService: true,
  },
}

const eslintConfig = defineConfig([
  {
    ignores: [
      "**/dist/",
      "**/node_modules/",
      "**/build/",
      "**/coverage/",
      "**/.turbo/",
      "**/out/",
      "**/out-tsc/",
      "**/temp/",
    ],
  },
  // JS
  {
    extends: [
      quiEslintTs.configs.base,
      quiEslintTs.configs.sortKeys,
      quiEslintTs.configs.styleGuide,
    ],
    // recommendation: scope these to your source files in your package(s).
    files: ["**/*.{js,mjs,cjs,ts,mts,cts}"],
  },
  // TS
  {
    extends: [
      ...quiEslintTs.configs.recommended,
      quiEslintTs.configs.performance,
      quiEslintTs.configs.strictExports,
    ],
    // recommendation: scope these to your source files in your package(s).
    files: ["**/*.ts"],
    languageOptions: tsLanguageOptions,
  },
  // React
  {
    extends: [
      ...quiEslintTs.configs.recommended,
      quiEslintTs.configs.performance,
      quiEslintReact.configs.base,
      quiEslintReact.configs.recommended,
      // optional: include the plugin as well
      quiEslintPluginReact.config,
    ],
    // recommendation: scope these to your source files in your package(s).
    files: ["**/*.{ts,tsx}"],
    languageOptions: tsLanguageOptions,
  },
])

export default eslintConfig