Skip to content
/ style Public

Style guide and configurations for tools in the web ecosystem.

License

Notifications You must be signed in to change notification settings

haltcase/style

Repository files navigation

@haltcase/style · npm version license @haltcase/style

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.

installation

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/style

Note

Some scenarios require additional peer dependencies. See the Prettier, ESLint, and Stylelint sections.

usage

Prettier

Note

Prettier is a peer dependency you'll need to install at the root of your project.

See: https://prettier.io/docs/en/install.html

Add the following in package.json:

{
	"prettier": "@haltcase/style/prettier"
}

ESLint

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.

ESLint customization

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

Configuring ESLint for TypeScript

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:

  1. Have a single tsconfig.json file in your root directory, including project references if necessary.

  2. Explicitly pass your tsconfig.json paths:

    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/

Configuring custom components for jsx-a11y

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"
				}
			}
		}
	}
];
A note on file extensions

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"
		}
	}
];

TypeScript

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.

TypeScript includes, excludes, and paths aliases

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.

Stylelint

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.

See: https://stylelint.io/user-guide/get-started

Add the following in package.json:

{
	"stylelint": {
		"extends": "@haltcase/style/stylelint"
	}
}

Tip

See the Stylelint documentation for other configuration options.

Stylelint configs

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

If you use Tailwind, you can additionally extend from @haltcase/style/stylelint/tailwind:

{
	"stylelint": {
		"extends": [
			"@haltcase/style/stylelint",
			"@haltcase/style/stylelint/tailwind"
		]
	}
}

Other workloads

Next.js

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-next

Then, 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:

// tsconfig.json
{
	"extends": "@haltcase/style/typescript/next"
}

contributing

Please read our contributing guide before creating a pull request.

About

Style guide and configurations for tools in the web ecosystem.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

No packages published

Contributors 3

  •  
  •  
  •