ESLint plugin
Five ESLint rules that catch invalid prop values, missing accessible names, a broken renderInput and sx colors that ignore dark mode.
@hone-ui/eslint-plugin flags wrong Hone UI usage while you lint. The rules read the props
manifest of the @hone-ui/react your app has installed, so they match the version you use.
@hone-ui/eslint-plugin is not published yet, so the install command below works only once the
package is on npm.
Install
The plugin needs ESLint 9 or later.
npm i -D eslint typescript-eslint @hone-ui/eslint-plugin
# or
pnpm add -D eslint typescript-eslint @hone-ui/eslint-pluginThe rules read JSX, so ESLint needs a parser that understands it. For TypeScript that is
typescript-eslint's parser, which the typescript-eslint package provides and the config below
imports. Skip typescript-eslint only if you lint plain JavaScript. Add the recommended config
after the parser.
import tseslint from 'typescript-eslint';
import hone from '@hone-ui/eslint-plugin';
export default [
{
files: ['**/*.{ts,tsx}'],
languageOptions: { parser: tseslint.parser },
},
hone.configs.recommended,
];For plain JavaScript, set languageOptions.parserOptions.ecmaFeatures.jsx to true instead of
using a parser. The recommended config turns on six rules as error and sx-hardcoded-color as
warn. To pick rules one by one, register the plugin under @hone-ui and set each rule in rules.
Rules
valid-prop-value
Reports a literal value that the component does not accept. It checks closed string unions and number props that have a minimum or maximum. The suggestion replaces the string with the closest valid value.
// Invalid
<Button size="huge">Save</Button>
// Valid
<Button size="large">Save</Button>icon-button-label
Requires aria-label or aria-labelledby on IconButton. A parent Tooltip with a text
title names the button, unless it sets describeChild. The rule has no fix, because only you
know the label.
// Invalid
<IconButton><DeleteIcon /></IconButton>
// Valid
<IconButton aria-label="Delete"><DeleteIcon /></IconButton>field-label
Requires an accessible name on TextField, Select, Slider, Checkbox, Radio, Switch,
Tabs, Dialog and the input that an Autocomplete renders. A <label htmlFor> in the same file
with a matching literal id also counts. The rule has no fix.
A Checkbox, Radio or Switch takes its name from a FormControlLabel around it, or from
aria-label or aria-labelledby in slotProps.input or inputProps. An aria-label on the toggle
itself lands on the root label, not on the input, so the rule reports it. Tabs takes aria-label
or aria-labelledby. A Dialog takes a DialogTitle child, aria-label or aria-labelledby; the
rule stays quiet when a child is a custom component or an expression, because either may render
the title.
Select takes its name from labelId, aria-label or aria-labelledby. Its label prop only
cuts the notch in the outline, so it does not count. Slider takes aria-label,
aria-labelledby or getAriaLabel.
// Invalid
<TextField />
// Valid
<TextField label="Email" />// Invalid
<Switch />
<Checkbox aria-label="Select row" />
// Valid
<FormControlLabel control={<Switch />} label="Wi-Fi" />
<Checkbox slotProps={{ input: { 'aria-label': 'Select row' } }} />render-input-params
Requires an Autocomplete renderInput to use its params. Without them the input loses its ref,
id and handlers. When the function returns a TextField, the suggestion inserts {...params}.
// Invalid
<Autocomplete options={options} renderInput={(params) => <TextField label="City" />} />
// Valid
<Autocomplete options={options} renderInput={(params) => <TextField {...params} label="City" />} />sx-scheme-function
Reports theme.palette inside an sx function. That object holds the values of one color
scheme, so the color stays fixed in dark mode. The suggestion rewrites it to theme.vars.palette,
which follows the active scheme. Members that exist only on theme.palette, such as mode and
getContrastText, are left alone.
// Invalid
<Box sx={(theme) => ({ color: theme.palette.primary.main })} />
// Valid
<Box sx={(theme) => ({ color: theme.vars.palette.primary.main })} />no-system-props
Reports a system prop such as fontWeight={600} or p={2} on Box, Stack and Typography.
Those components do not support system props, so the value does nothing. Move it into sx. The
rule has no fix. width, height and border are left alone because they are plain HTML
attributes, and so is any prop the component owns.
// Invalid
<Typography fontWeight={600}>Title</Typography>
// Valid
<Typography sx={{ fontWeight: 600 }}>Title</Typography>sx-hardcoded-color
Reports a literal hex, rgb(), hsl() or similar color in sx, on color keys such as color,
bgcolor and borderColor and in border and background shorthands. A fixed color ignores the
theme and dark mode. Use a palette key such as primary.main, or a var(--hn-palette-*)
variable. Named colors and var() values are not reported.
// Invalid
<Box sx={{ color: '#ff0000' }} />
// Valid
<Box sx={{ color: 'error.main' }} />How the rules find @hone-ui/react
A rule reads node_modules/@hone-ui/react/dist/editor-manifest.json. It starts in the folder of
the file being linted and walks up to the nearest node_modules/@hone-ui/react. In a monorepo,
each app is checked against its own installed version. A file with no absolute path, as with
eslint --stdin, starts from ESLint's working folder.
The plugin caches the manifest and reloads it when its modification time changes, so an upgrade
needs no ESLint restart. If @hone-ui/react is not installed there, or its manifest is missing,
valid-prop-value reports nothing. The label rules do not need the manifest.
A rule tracks only components imported from @hone-ui/react. It accepts a named import, a
namespace import and a default import from a subpath such as @hone-ui/react/Button. Renamed
imports work. Type-only imports, local components and names that an inner scope shadows are
ignored.
What the rules skip
The rules report only what they can read. When they cannot know, they stay quiet.
- An element with a spread attribute, such as
<IconButton {...props} />. The spread may carry the label or the value. - A value that is not a literal, such as
size={size}, a template string with an expression or a conditional. - An open union, such as
color, which accepts any palette key, and a prop typed as a plain string. - An object prop that is a variable or holds a spread, such as
inputProps={{ ...p }}. It may carry the name. - A
renderInputthat is not an inline function, destructures its first parameter or returns something other than JSX. - A
renderInputelement that spreads something besides the params. - An
sxfunction that destructures its theme parameter.
AI guide
An AI guide for the plugin ships at @hone-ui/eslint-plugin/ai/eslint-plugin.md and is served at
/ai/eslint-plugin.md. The MCP server runs the same rules
through its check_usage tool.