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.
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:
scripts/configure-git.mjsruns after everypnpm installand setscore.ignorecase = falsein your local clone, so git tracks case-only renames even though APFS doesn't..husky/pre-commitrejects 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
| Entity | Casing | Example | Why |
|---|---|---|---|
Component file (.tsx) | PascalCase | Button.tsx, ColorPalette.tsx | Matches the default export and shadcn/ui's convention |
| Non-component file | kebab-case | generate-nav.ts, bridge-concepts.ts | Filesystem-friendly, no case-sensitivity surprises across macOS/Linux |
| Directory | kebab-case | color-palette/, getting-started/ | URL-safe, matches Next.js routing segments |
| React component | PascalCase | function Button(), <DataTable /> | Required by JSX — lowercase tags are treated as DOM elements |
| React hook | camelCase + use prefix | useTheme, useDarkMode | Required by the Rules of Hooks |
| Function / variable | camelCase | normalizeUrl, defaultShade, isLoading | JavaScript standard |
| Module-level constant | UPPER_SNAKE_CASE | COLOR_SCALES, SHADES | Signals immutability and module-level scope |
| Type / interface | PascalCase | NavItem, Theme, ApiResponse | TypeScript standard |
| Component prop interface | PascalCase + Props suffix | ButtonProps, ColorPaletteProps | Findable via grep, paired with the component |
| Tailwind class composition | kebab-case | bg-primary text-primary-foreground | Tailwind defaults |
| CSS variable | kebab-case with -- | --color-brand-600, --radius-md | CSS spec |
| Environment variable | UPPER_SNAKE_CASE | NEXT_PUBLIC_GATEWAY_URL | Shell 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.
const colorScale = "brand" function parseFrontmatter(input: string) { /* ... */ } const isLoading = true const handleSubmit = () => { /* ... */ }
PascalCase
Like camelCase but the first letter is also capitalised. Sometimes called UpperCamelCase.
Used for: React components, classes, types, interfaces, enums, type-only generics.
function ColorPalette() { /* ... */ } class HttpClient { /* ... */ } type NavItem = { /* ... */ } interface ColorPaletteProps { /* ... */ }
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.
const COLOR_SCALES = ["brand", "alt", "sun", "jeans", "gray"] as const const MAX_RETRIES = 3 process.env.NEXT_PUBLIC_GATEWAY_URL
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 file →
PascalCase.tsx. The filename matches the default-exported component:Button.tsxexportsButton. 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 file →
kebab-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 file →
kebab-case.ts(e.g.types.tsornav.types.ts). It's not a component. - Barrel →
index.ts.
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:
- Automatic git config.
pnpm installrunsscripts/configure-git.mjsafter husky setup, which setscore.ignorecase = falsein 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. - Pre-commit collision check.
.husky/pre-commitrejects commits that contain two paths differing only by case (e.g. bothcolor-palette.tsxandColorPalette.tsxstaged 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:
git mv ColorPalette.tsx _tmp.tsx git mv _tmp.tsx color-palette.tsx git commit
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
onprefix:onClick,onSubmit,onSelectionChange. - Event handlers as locals use
handleprefix:handleClick,handleSubmit. Theonform 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 itfetchUser. - 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 forButtonPropsand 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 likeTypeorEnumunless they add information. - Generic type parameters use single letters for simple cases (
T,K,V) and descriptive PascalCase prefixed withTfor 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:
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
Local constants inside a function or component stay camelCase — the const keyword already says they don't reassign:
function buildSection() { const sectionTitle = "Contributing" const items = scanDirectory() return { title: sectionTitle, items } }
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, notHTTPClient.parseUrl, notparseURL. 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
camelCasefor field IDs by convention. Match it. - WordPress meta keys — WordPress uses
snake_casefor 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 tocamelCasefor 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.