Naming conventions

Casing styles for files, folders, components, variables, and types — what to use where, and why.

This page is the canonical reference for how things get named in this repo. The patterns — which casing applies to which kind of entity — are held together by code review, not by ESLint. The mechanics of case-only file renames, on the other hand, are now hook-enforced so that you can rename files freely on macOS without silently dropping the change. Following the patterns keeps the codebase searchable, makes diffs predictable, and lowers the cost of jumping between packages.

What's enforced and what isn't

Patterns are review-only. There is no @typescript-eslint/naming-convention rule wired into packages/eslint-config yet, so nothing automatically rejects a my_function_name or a BUTTON.tsx. Reviewers will. If the rules here disagree with what reviewers actually accept, the reviewers win — open a PR to update the page.

Case-rename safety is hook-enforced. Two pieces of automation make case renames reliable on macOS:

  1. scripts/configure-git.mjs runs after every pnpm install and sets core.ignorecase = false in your local clone, so git tracks case-only renames even though APFS doesn't.
  2. .husky/pre-commit rejects any commit that contains two paths differing only by case (a half-finished rename), with a clear error message and the recovery recipe.

Together these mean: adopt the patterns below by hand, but trust the tooling to make any future renames stick. See the Renaming a file's casing (macOS gotcha) section below for the details.

Quick reference

EntityCasingExampleWhy
Component file (.tsx)PascalCaseButton.tsx, ColorPalette.tsxMatches the default export and shadcn/ui's convention
Non-component filekebab-casegenerate-nav.ts, bridge-concepts.tsFilesystem-friendly, no case-sensitivity surprises across macOS/Linux
Directorykebab-casecolor-palette/, getting-started/URL-safe, matches Next.js routing segments
React componentPascalCasefunction Button(), <DataTable />Required by JSX — lowercase tags are treated as DOM elements
React hookcamelCase + use prefixuseTheme, useDarkModeRequired by the Rules of Hooks
Function / variablecamelCasenormalizeUrl, defaultShade, isLoadingJavaScript standard
Module-level constantUPPER_SNAKE_CASECOLOR_SCALES, SHADESSignals immutability and module-level scope
Type / interfacePascalCaseNavItem, Theme, ApiResponseTypeScript standard
Component prop interfacePascalCase + Props suffixButtonProps, ColorPalettePropsFindable via grep, paired with the component
Tailwind class compositionkebab-casebg-primary text-primary-foregroundTailwind defaults
CSS variablekebab-case with ----color-brand-600, --radius-mdCSS spec
Environment variableUPPER_SNAKE_CASENEXT_PUBLIC_GATEWAY_URLShell convention

Casing styles glossary

If you've never had to spell out the difference between these in a code review, here's the cheat sheet.

camelCase

Lower-case first word, capital first letter of every subsequent word, no separators.

Used for: variables, function names, function parameters, props, methods, object keys.

ts
const colorScale = "brand"
function parseFrontmatter(input: string) { /* ... */ }
const isLoading = true
const handleSubmit = () => { /* ... */ }
4 lines of ts code

PascalCase

Like camelCase but the first letter is also capitalised. Sometimes called UpperCamelCase.

Used for: React components, classes, types, interfaces, enums, type-only generics.

ts
function ColorPalette() { /* ... */ }
class HttpClient { /* ... */ }
type NavItem = { /* ... */ }
interface ColorPaletteProps { /* ... */ }
4 lines of ts code

kebab-case

All lowercase, words separated by hyphens. Sometimes called dash-case or lisp-case.

Used for: non-component file names, directory names, CSS classes, CSS custom properties (with -- prefix), URL segments, npm package names.

generate-nav.ts
color-palette/
bg-primary text-primary-foreground
--color-brand-600
/developers/design-system
@itu/docs-ui

snake_case

All lowercase, words separated by underscores. Rare in this repo.

Used for: external integrations where the destination dictates the casing — Postgres column names, Python scripts, some shell variables.

user_id
created_at

UPPER_SNAKE_CASE

All uppercase, words separated by underscores. Sometimes called SCREAMING_SNAKE_CASE or MACRO_CASE.

Used for: module-level constants, environment variables.

ts
const COLOR_SCALES = ["brand", "alt", "sun", "jeans", "gray"] as const
const MAX_RETRIES = 3
process.env.NEXT_PUBLIC_GATEWAY_URL
3 lines of ts code

File naming in detail

The split between PascalCase and kebab-case for files comes down to one question: is this file primarily a React component?

  • Component filePascalCase.tsx. The filename matches the default-exported component: Button.tsx exports Button. Sub-components used only inside the file stay private; if a file ends up with multiple exports, the one that gives the file its identity sets the name.
  • Non-component TypeScript filekebab-case.ts. Utility modules, scripts, configs, type declarations, MDX components that wrap components but aren't themselves React components — anything that isn't "this is a React component" gets kebab-case.
  • Story file<Component>.stories.tsx. PascalCase, matches its component.
  • Test file<source>.test.ts(x) or <source>.spec.ts(x). Match the source file's casing.
  • Type-only filekebab-case.ts (e.g. types.ts or nav.types.ts). It's not a component.
  • Barrelindex.ts.
Shadcn primitives keep lowercase filenames

The shadcn/ui primitives under packages/<pkg>/src/components/ui/ (e.g. accordion.tsx, card.tsx, tooltip.tsx) intentionally stay lowercase. That's the convention shadcn ships them in, and reversing it would fight every pnpm <pkg>:add invocation. Treat the ui/ subfolder as an exception — every other component file in the repo follows PascalCase.

Renaming a file's casing (macOS gotcha)

macOS's default filesystem is case-insensitive but case-preserving. Combined with git's default core.ignorecase = true, this means a git mv color-palette.tsx ColorPalette.tsx on macOS is silently a no-op — the OS sees the source and destination as the same file, git agrees, and your rename never reaches the remote.

The repo neutralises this in two ways:

  1. Automatic git config. pnpm install runs scripts/configure-git.mjs after husky setup, which sets core.ignorecase = false in your local clone. With this set, git tracks case-only renames even though the underlying filesystem doesn't. You don't have to do anything — fresh clones get configured automatically.
  2. Pre-commit collision check. .husky/pre-commit rejects commits that contain two paths differing only by case (e.g. both color-palette.tsx and ColorPalette.tsx staged at once). This catches half-finished renames before they reach the remote, where they would silently break on case-sensitive Linux runners.

If you ever need to do the rename manually — on Windows, on a case-insensitive Linux mount, or in a checkout where the auto-config didn't run — use the bulletproof two-step pattern:

bash
git mv ColorPalette.tsx _tmp.tsx
git mv _tmp.tsx color-palette.tsx
git commit
3 lines of bash code

The intermediate name is the trick: it forces a real filesystem-level rename that git can see, regardless of core.ignorecase.

Variable and function naming

A few patterns worth knowing beyond the bare casing rules:

  • Booleans read as predicates: prefix with is, has, should, can. Examples: isOpen, hasError, shouldFetch, canEdit.
  • Event handlers as props use on prefix: onClick, onSubmit, onSelectionChange.
  • Event handlers as locals use handle prefix: handleClick, handleSubmit. The on form is reserved for the prop boundary so the call site reads naturally: <Button onClick={handleClick} />.
  • Async functions don't get a special prefix — the return type is the signal. Don't name them fetchUserAsync; just call it fetchUser.
  • Single-letter names are only acceptable for tight loops (i, j) or destructuring shorthand. Everywhere else, give it a real name.

Types and interfaces

  • Component props always use <ComponentName>Props. This makes it grep-findable and pairs cleanly with the component: search for ButtonProps and you find both the type definition and every consumer that imports it. Examples: ButtonProps, ColorPaletteProps, NavItemProps.
  • Discriminated unions use a descriptive PascalCase name: Theme, ApiResponse, BlockType. Avoid generic suffixes like Type or Enum unless they add information.
  • Generic type parameters use single letters for simple cases (T, K, V) and descriptive PascalCase prefixed with T for complex cases (TItem, TKey). The single-letter form is preferred when the generic is unconstrained.

Constants

Constants whose value is fixed at module load time and acts as configuration use UPPER_SNAKE_CASE:

ts
const COLOR_SCALES = ["brand", "alt", "sun", "jeans", "gray"] as const
const SHADES = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950] as const
const MAX_RETRIES = 3
3 lines of ts code

Local constants inside a function or component stay camelCase — the const keyword already says they don't reassign:

ts
function buildSection() {
const sectionTitle = "Contributing"
const items = scanDirectory()
return { title: sectionTitle, items }
}
5 lines of ts code

The rule of thumb: if you'd put it in a config file, it's UPPER_SNAKE_CASE. If you'd inline it, it's camelCase.

What about everything else?

A few edge cases that come up often enough to call out:

  • Acronyms in PascalCase / camelCase keep only the first letter capital after the first word: HttpClient, not HTTPClient. parseUrl, not parseURL. This makes the boundaries between words readable at a glance.
  • Carbon icon names — Carbon ships icons in PascalCase (e.g. <Code />, <DataTable />). Use them as imported, don't rename.
  • Payload field names — Payload CMS uses camelCase for field IDs by convention. Match it.
  • WordPress meta keys — WordPress uses snake_case for post meta and option keys. Don't fight it; mirror what WP expects.
  • Database columns — Postgres column names are snake_case. The Zod schemas at the gateway boundary translate to camelCase for the JS side.

Related

  • Commit messages — the format your commit subject follows is its own little convention.
  • Branch naming<type>/<kebab-case> for branches, mirroring the file convention.