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.
pnpm add -D @qualcomm-ui/eslint-plugin-react
npm i --save-dev @qualcomm-ui/eslint-plugin-react
yarn add -D @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:
IconButtonInlineIconButtonHeaderBarActionIconButton
// 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:
| Component | Composite Element | Composite Props |
|---|---|---|
TextInput | Input | inputProps |
NumberInput | Input | inputProps |
PasswordInput | Input | inputProps |
Combobox | Control | inputProps |
Select | Control | controlProps |
Switch | HiddenInput | hiddenInputProps |
Checkbox | HiddenInput | hiddenInputProps |
Radio | HiddenInput | hiddenInputProps |
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
pnpm add -D @qualcomm-ui/eslint-config-typescript @qualcomm-ui/eslint-config-react
npm i --save-dev @qualcomm-ui/eslint-config-typescript @qualcomm-ui/eslint-config-react
yarn add -D @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:
| Config | Description |
|---|---|
base | Core TypeScript rules |
recommended | Includes all configs below |
styleGuide | Code style enforcement |
sortKeys | Object key ordering |
typeChecks | Type-aware linting rules |
namingConventions | Identifier naming patterns |
performance | Performance-related rules |
strictExports | Export restrictions |
jsdoc | JSDoc comment validation |
Style Guide Rules
The styleGuide config enforces these conventions:
curly- Require braces for all control statementseqeqeq- Require===and!==(except fornullcomparisons)@stylistic/spaced-comment- Balanced spacing in comments@typescript-eslint/explicit-member-accessibility- Require explicit visibility modifiers, but omitpubliccomment-length/*- Maximum 85 characters per comment lineobject-shorthand- Require shorthand syntax for object propertiesprefer-template- Require template literals over string concatenationprefer-const- Requireconstfor variables that are never reassignedno-var- Disallowvardeclarations
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 specifiersunused-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:
| Config | Description |
|---|---|
base | Core React rules |
recommended | Base config plus hooks and JSX rules |
strict | Recommended plus React Compiler rules |
Recommended Rules
The recommended config includes:
react-hooks/rules-of-hooks- Enforce Rules of Hooksreact-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 propsreact/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 functionsreact-hooks/immutability- Enforce immutable data patternsreact-hooks/use-memo- Validate useMemo usagereact-hooks/preserve-manual-memoization- Respect manual memoizationreact-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