camelCase vs snake_case vs kebab-case: Which to Use (2026)
camelCase vs snake_case vs kebab-case is rarely a taste question — each one is enforced somewhere. Here is the 2026 map of which convention goes where.
camelCase vs snake_case vs kebab-case: Which to Use
Every codebase argues about this once. The argument is usually pointless, because for most identifiers the choice has already been made — by a style guide, a parser, or a filesystem. camelCase, snake_case, and kebab-case are not interchangeable aesthetics; each is required in specific places and illegal in others. This guide maps which goes where, and what breaks when you guess.
TL;DR
- Hyphens parse as minus, so kebab-case is illegal in most language identifiers.
- Python and Rust use snake_case for values; Java and JavaScript use lowerCamelCase.
- kebab-case owns CSS selectors, file names, URL paths, and npm package names.
- SCREAMING_SNAKE_CASE is for constants and environment variables, nothing else.
- Converting between them is lossy — pick one canonical form and generate the rest.
The five conventions, defined
There are really five, not three. Grouping PascalCase under camelCase and SCREAMING_SNAKE_CASE under snake_case hides the two distinctions that cause most bugs.
camelCase and PascalCase
lowerCamelCase starts lowercase and capitalises every subsequent word: totalItemCount. UpperCamelCase — almost always called PascalCase — capitalises the first word too: TotalItemCount. Nearly every style guide that uses camel case splits the two by role: PascalCase for things that are types, lowerCamelCase for things that are values.
snake_case and SCREAMING_SNAKE_CASE
snake_case joins lowercase words with underscores: total_item_count. SCREAMING_SNAKE_CASE (also written UPPER_SNAKE_CASE or CONSTANT_CASE) uppercases everything: TOTAL_ITEM_COUNT. The uppercase variant is not a stylistic sibling — it carries semantic weight. In most ecosystems it means "this value is fixed at load time and never mutates."
kebab-case
kebab-case joins lowercase words with hyphens: total-item-count. It is the odd one out, because a hyphen is a subtraction operator in nearly every C-descended language. total-item-count in JavaScript is three variables minus each other. That single fact explains almost all of kebab-case's territory: it appears exactly where the string is not an identifier being evaluated.
| Convention | Word separator | Typical role |
|---|---|---|
| lowerCamelCase | capital letter | variables, methods |
| PascalCase | capital letter | classes, types |
| snake_case | underscore | functions, columns |
| SCREAMING_SNAKE | underscore | constants, env vars |
| kebab-case | hyphen | files, URLs, CSS |
Which naming convention should I use in JavaScript?
JavaScript is the language where all five show up in one project, often in one file, which is why the question gets asked so often.
Variables, functions, and object properties
lowerCamelCase, without exception in practice. PascalCase is reserved for constructors, classes, React components, and TypeScript type and interface names — the capital is load-bearing in JSX, where <button> renders an HTML element and <Button> renders your component. Module-level constants that are genuinely immutable get SCREAMING_SNAKE_CASE; a const binding to a mutable object does not qualify, and styling it as a constant misleads readers.
Java's style guide is worth borrowing here because it is unusually explicit about what "constant" means. The Google Java Style Guide restricts UPPER_SNAKE_CASE to static final fields whose contents are deeply immutable and whose methods have no detectable side effects — a static final Set built from new HashSet<>() is not a constant, because its observable state can still change. The same test works in JavaScript: if you can push to it, it is not a constant.
File names: why kebab-case wins
File names are not identifiers, so the hyphen restriction does not apply — and three separate forces push toward kebab-case:
- Case-insensitive filesystems. macOS (APFS, default configuration) and Windows treat
UserCard.jsandusercard.jsas the same file. Linux CI does not. A rename that only changes case can commit cleanly on a laptop and fail the build. - URLs. Static site generators and most bundlers map file paths to routes directly. A camelCase file becomes a camelCase URL.
- Search and shell. Hyphenated names tab-complete predictably and survive being pasted into a terminal without quoting.
The common exception is React: many teams name component files UserCard.tsx to match the exported component. That is defensible, but it is a deliberate trade against the case-insensitivity risk above — pick one rule per repo and enforce it in CI rather than leaving it to habit.
JSON payloads and the camelCase / snake_case border
This is where most real friction lives: a Python or Rails backend produces created_at, and a JavaScript frontend wants createdAt. Both sides are following their own conventions correctly.
Three workable answers, in order of preference:
- Convert once, at the API boundary — a serializer on the way out, an interceptor on the way in.
- Pick the backend's convention and use bracket access or generated types on the client.
- Convert by hand in each component. (This is the one that produces
user_idanduserIdin the same object six months later.)
If you are transforming an existing export rather than writing a serializer, our CSV ↔ JSON converter will round-trip the structure while you rename keys, and the case converter handles the key list itself.
Why does CSS use kebab-case instead of camelCase?
Because CSS has no arithmetic in its property grammar, so a hyphen is unambiguous — and because the syntax predates the DOM's JavaScript mirror by years.
The DOM keeps a camelCase copy
background-color in a stylesheet is element.style.backgroundColor in JavaScript. The DOM had no choice: element.style.background-color parses as a subtraction. The same mapping applies to data attributes — data-user-id in HTML becomes dataset.userId in script. Knowing that the two spellings are the same property, mechanically transformed, removes most of the confusion.
Custom properties are case-sensitive — this bites
Regular CSS property names are case-insensitive. Custom properties are not. Per MDN's guide to CSS custom properties, --my-color and --My-color are two distinct properties, and var(--foo) will not resolve a variable declared as --FOO. There is no fallback, no warning, and no error — the declaration is simply invalid at computed-value time and you get the inherited or initial value instead.
MDN notes the case sensitivity extends past ASCII: two visually identical names can differ if one uses a precomposed accented character and the other uses a base letter plus a combining accent. That is a rare bug, but an unfindable one.
The practical rule for design tokens
Use lowercase kebab-case for every custom property, with the most general segment first: --color-surface-raised, not --raisedSurfaceColor. The prefix ordering matters more than the casing — it makes tokens sort into groups in an editor's autocomplete.
Can you use camelCase in a URL?
You can, and you should not.
npm made the decision for you
npm's package name guidelines are blunt: a package name must not contain uppercase letters. npm's own package.json documentation gives the reason — the name ends up as part of a URL, an argument on the command line, and a folder name, so it cannot contain anything that is not URL-safe. Packages published before the rule took effect still carry capitals, which is why you occasionally see a mixed-case name in a lockfile. That is legacy, not license.
Paths are case-sensitive, hosts are not
The host portion of a URL is case-insensitive; the path is not. /UserProfile and /userprofile are different resources as far as the spec is concerned, and whether they resolve to the same page depends entirely on your server. Two URLs that render the same page are a duplicate-content problem, and a link typed with the wrong case is a 404 that never reproduces on the developer's machine.
Query-string keys inherit the same trap. If you are debugging one, decoding the raw query first usually makes the mismatch obvious — that is what the URL encoder / decoder is for.
Where the choice is not yours
Some contexts enforce a convention through tooling or through the runtime. These are not preferences.
Language style guides
| Language | Types | Values / functions |
|---|---|---|
| Python | PascalCase | snake_case |
| Rust | UpperCamelCase | snake_case |
| Java | UpperCamelCase | lowerCamelCase |
| Go | MixedCaps | MixedCaps / mixedCaps |
| Ruby | PascalCase | snake_case |
PEP 8 states the Python row directly: function and variable names are lowercase with underscores as needed for readability, and mixedCase is acceptable only where it is already the prevailing style in a module, for backwards compatibility.
The Rust API Guidelines add two rules that automated converters routinely break. First, acronyms count as a single word: the standard library spells it Uuid, not UUID, and Stdin, not StdIn. Second, a "word" in snake_case should never be a single letter unless it is the last one — hence btree_map rather than b_tree_map, but PI_2 rather than PI2.
Google's Java guide encodes the acronym rule as an algorithm: lowercase everything including acronyms, then re-uppercase only the first letter of each word. That turns "XML HTTP request" into XmlHttpRequest and "supports IPv6 on iOS" into supportsIpv6OnIos. It looks wrong the first time and is far more predictable than the alternative.
Environment variables and the shell
SCREAMING_SNAKE_CASE, and here it is close to a hard requirement rather than a convention. POSIX shells define portable environment variable names as uppercase letters, digits, and underscores, not starting with a digit. Lowercase names generally work in bash and zsh, but tooling — Docker, systemd, CI runners, container orchestrators — is written expecting the uppercase form, and a hyphen is not usable at all, since export MY-VAR=1 is a syntax error.
Databases fold unquoted identifiers
PostgreSQL lowercases every unquoted identifier. Create a column as createdAt and it becomes createdat; query it as createdAt and it still resolves to createdat, so it appears to work until someone quotes it as "createdAt" and gets a column-does-not-exist error. snake_case sidesteps the whole problem because it survives folding unchanged. MySQL's behaviour additionally depends on the host filesystem's case sensitivity for table names, which is its own class of production-only bug.
How to convert without breaking acronyms
Conversion is mechanical, and it is lossy in one direction.
camelCase to kebab-case
const toKebab = (s) =>
s.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
.toLowerCase();
toKebab('backgroundColor'); // background-color
toKebab('XMLHttpRequest'); // xml-http-request
toKebab('parse2Files'); // parse2-files
The second replace is the one people omit. Without it, a run of capitals collapses into a single segment and XMLHttpRequest becomes xmlhttp-request. If the capture-group syntax is unfamiliar, our walkthrough of regex capture groups covers $1 and named references, and you can test either pattern against your own identifier list in the regex tester.
kebab-case to camelCase
const toCamel = (s) =>
s.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
toCamel('background-color'); // backgroundColor
toCamel('data-user-id'); // dataUserId
This direction is clean because the separator is explicit. The reverse is not: userId, userID, and user_id all convert to the same kebab string, so a round trip cannot restore the original. That asymmetry is the argument for choosing one canonical spelling per boundary — usually the backend's — and generating every other form from it, rather than converting in both directions.
The decision table
| If the name is a… | Use |
|---|---|
| Class, type, React component | PascalCase |
| Variable, function, JSON key (JS) | lowerCamelCase |
| Python / Rust function, SQL column | snake_case |
| Constant, environment variable | SCREAMING_SNAKE_CASE |
| File, URL path, CSS class, package | kebab-case |
References
- PEP 8 – Style Guide for Python Code — cited for Python's snake_case rule for functions and variables, and the mixedCase backwards-compatibility exception.
- Rust API Guidelines — Naming — source for the type-level vs value-level split, the acronym-as-one-word rule, and the single-letter-word rule.
- Google Java Style Guide — used for the constant definition and the deterministic camel-case algorithm with its IPv6 / iOS examples.
- Using CSS custom properties (variables) — MDN — confirmed that custom property names are case-sensitive, unlike other CSS properties.
- Package name guidelines | npm Docs — the rule that a package name must not contain uppercase letters.
- package.json | npm Docs — the URL / CLI argument / folder-name reasoning behind npm's lowercase requirement.
Related on iKit
- Converting camelCase identifiers to snake_case without breaking code — the full regex walkthrough for the direction this guide only summarises, including the Postgres folding trap.
- Going the other way: snake_case to camelCase with acronyms intact — why
user_idanduser_uuidneed different rules, and how to keepIDfrom becomingId. - Regex capture groups explained: $1, $&, and named references — the substitution syntax both conversion snippets above depend on.
- Regex find and replace in VS Code — how to apply a naming-convention change across a whole codebase in one pass instead of file by file.
Related posts
REST API Naming: snake_case Backend, camelCase Frontend (2026)
REST API naming forces a choice: snake_case on the backend, camelCase in the client. Here is where to convert, what the round trip loses, and how to decide.
Why Designers Still Use Lorem Ipsum in 2026 (And When Not)
Lorem ipsum survived sixty years of design tooling for one reason: it fails loudly. Here is what filler text is genuinely good at, and where it costs you.
Lorem Ipsum for i18n Testing: Catch Text Overflow (2026)
Lorem Ipsum passes every layout test, then German breaks it. Here is why Latin filler hides i18n bugs and what to paste instead before you localise.