Style guide and configurations for tools in the web ecosystem.
This package originated from Vercel's style guide, but has since diverged in both architecture and rules.
Install @haltcase/style with your preferred package manager:
# pnpm
pnpm add --save-dev @haltcase/style
# npm
npm i --save-dev @haltcase/style
# yarn
yarn add --dev @haltcase/styleNote
Some scenarios require additional peer dependencies. See the Prettier, ESLint, and Stylelint sections.
Note
Prettier is a peer dependency you'll need to install at the root of your project.
Add the following in package.json:
{
"prettier": "@haltcase/style/prettier"
}Note
ESLint is a peer dependency you'll need to install at the root of your project.
See: https://eslint.org/docs/user-guide/getting-started#installation-and-usage
Important
This config requires ESLint 9+ and the use of flat config.
At its simplest, your config might look like this for a general purpose project:
// eslint.config.js
import { getEslintConfig } from "@haltcase/style/eslint";
export default getEslintConfig();TypeScript support is enabled by default by applying TypeScript-specific
configurations to files with TypeScript file extensions
(ts, tsx, mts, and mtsx). You may need to apply your own project-specific
configurations — see Configuring ESLint for TypeScript
for details on how to do so.
You can enable more rules for your project with configuration options:
// eslint.config.js
import { getEslintConfig } from "@haltcase/style/eslint";
export default getEslintConfig({
// for browser environments
browser: true,
// treat *.js files as CommonJS instead of ES Modules
commonjs: true,
// for Next.js projects (implies `react`, requires `@next/eslint-plugin-next`)
nextjs: true,
// for Node.js projects
node: true,
// for React
react: true
});And, of course, you may include configs as any part of a larger one:
// eslint.config.js
import { getEslintConfig } from "@haltcase/style/eslint";
export default [
...getEslintConfig({
nextjs: true
}),
{
name: "Customizations",
files: ["src/lib/**"],
rules: {
"no-unused-vars": "warn"
}
}
];You can be more granular with your customizations by using the modular configs
also exported from @haltcase/style/eslint:
// eslint.config.js
import {
getEslintBaseConfig,
getEslintBrowserConfig,
getEslintNodeConfig,
getEslintReactConfig,
withFiles
} from "@haltcase/style/eslint";
export default [
// use the base config for everything by default
...getEslintBaseConfig(),
// apply browser rules to some files
...getEslintBrowserConfig().map((config) => ({
...config,
files: ["src/client-only/**/*.js"]
})),
// apply React rules to some files
// (`withFiles` is equivalent to the above `map`)
...withFiles(getEslintReactConfig(), ["src/frontend/**/*.{ts,js,tsx,jsx}"]),
...withFiles(getEslintNodeConfig(), ["src/backend/**/*.{ts,js}"]),
{
name: "Customizations",
files: ["src/lib/**"],
rules: {
"no-unused-vars": "warn"
}
}
];Tip
See the ESLint documentation for more details on using configs: https://eslint.org/docs/latest/use/configure/combine-configs
Some of the rules enabled in the TypeScript config require additional type
information. By default, this config will configure parserOptions with
project: true.
You can customize this behavior with the typescriptProject option:
import { getEslintConfig } from "@haltcase/style/eslint";
export default [
...getEslintConfig({
typescriptProject: [
"./packages/**/tsconfig.json",
"./separate-package/tsconfig.json"
]
})
];You can also enable TypeScript ESLint's
projectService:
import { getEslintConfig } from "@haltcase/style/eslint";
export default [
...getEslintConfig({
typescriptProject: {
projectService: true
}
})
];Because eslint-import-resolver-typescript does not support
projectService,
you should either:
-
Have a single
tsconfig.jsonfile in your root directory, including project references if necessary. -
Explicitly pass your
tsconfig.jsonpaths:import { getEslintConfig } from "@haltcase/style/eslint"; export default [ ...getEslintConfig({ typescriptProject: { projectService: true, tsconfigPaths: [ "./packages/**/tsconfig.json", "./separate-package/tsconfig.json" ] } }) ];
If you need to further customize the TypeScript behavior, use a separate config with your own options, for example:
import { getEslintConfig } from "@haltcase/style/eslint";
export default [
...getEslintConfig(),
{
files: ["*.{c,m,}ts{x,}"],
parserOptions: {
project: true,
tsConfigRootDir: "/custom/config/directory"
}
}
];For more information, see: https://typescript-eslint.io/linting/typed-linting/
It's common practice for React apps to have shared components like Button
that wrap native elements. You can pass this information along to jsx-a11y
via the components setting.
The below list is not exhaustive.
import { getEslintConfig } from "@haltcase/style/eslint";
export default [
getEslintConfig({
react: true
}),
{
settings: {
"jsx-a11y": {
components: {
Article: "article",
Button: "button",
Image: "img",
Input: "input",
Link: "a",
Video: "video"
}
}
}
}
];By default, all TypeScript rules are scoped to files with extensions ending in
ts or tsx.
If you need to override configuration for TypeScript files, make sure to
specify the files property or ESLint will
only target *.js files.
export default [
...getEslintConfig(),
{
// apply this config to js, ts, jsx, and tsx files
files: ["directory/**/*.[jt]s?(x)"],
rules: {
"my-rule": "off"
}
}
];Several Typescript configs are available to cover various scenarios:
| Name | Description |
|---|---|
@haltcase/style/typescript/base |
Baseline config, intended to be extended from. |
@haltcase/style/typescript/bundler |
For use in bundled projects, most commonly Vite + React. |
@haltcase/style/typescript/next |
For use in Next.js projects. See the Next.js section for more details. |
@haltcase/style/typescript/node |
Default Node config, currently targeting Node 24. |
@haltcase/style/typescript/node-ts |
Addon for other Node configs for use with Node's built-in TypeScript support. |
@haltcase/style/typescript/node18 |
For projects targeting Node 18. |
@haltcase/style/typescript/node20 |
For projects targeting Node 20. |
@haltcase/style/typescript/node22 |
For projects targeting Node 22. |
@haltcase/style/typescript/node24 |
For projects targeting Node 24. |
@haltcase/style/typescript/web |
For use in web projects. |
Typically, you'll only need to extend from one of these:
{
"extends": "@haltcase/style/typescript/node"
}You could also combine them with an extends array in Typescript 5+:
{
"extends": [
"@haltcase/style/typescript/node",
"@haltcase/style/typescript/web"
]
}Tip
Run tsc --showConfig to see the result of the combined configs.
Some TypeScript configs set include, exclude, and paths for you with
common options. Currently:
| Name | Description |
|---|---|
@haltcase/style/typescript/bundler |
Includes @/* → src/* path alias. |
@haltcase/style/typescript/next |
Includes @/* → src/* path alias and Next.js include and exclude settings. |
To format and check CSS with Stylelint, install the stylelint package and
set your configuration to extend from @haltcase/style/stylelint.
Note
Stylelint is a peer dependency you'll need to install at the root of your project.
Add the following in package.json:
{
"stylelint": {
"extends": "@haltcase/style/stylelint"
}
}Tip
See the Stylelint documentation for other configuration options.
There are several configs you can extend from. The default config does not
enforce a specific naming convention for classes, IDs, etc. This is intended
to allow flexibility between conventional CSS, where kebab-case is standard,
and CSS Modules or CSS-in-JS, where camelCase is more common.
There are alternate entry points if you would like to enforce a specific naming
convention: standard for kebab case and modules for camel case.
| Name | Description |
|---|---|
@haltcase/style/stylelint |
Do not enforce a naming convention for identifiers. |
@haltcase/style/stylelint/modules |
Enforce camelCase identifiers. |
@haltcase/style/stylelint/standard |
Enforce conventional kebab-case identifiers. |
If you use Tailwind, you can additionally extend from @haltcase/style/stylelint/tailwind:
{
"stylelint": {
"extends": [
"@haltcase/style/stylelint",
"@haltcase/style/stylelint/tailwind"
]
}
}For Next.js projects, install @next/eslint-plugin-next as a development
dependency.
Tip
Keep your Next.js and @next/eslint-plugin-next versions in sync for best
compatibility.
# pnpm
pnpm add -D @next/eslint-plugin-next
# npm
npm i -D @next/eslint-plugin-nextThen, enable the Next.js config in your ESLint configuration:
// eslint.config.js
import { getEslintConfig } from "@haltcase/style/eslint";
export default getEslintConfig({
nextjs: true
});See the ESLint section for more details.
If you're using TypeScript, you can extend from the Next.js-specific config:
Please read our contributing guide before creating a pull request.