Library / SDK
sindresorhus/camelcase avatar
sindresorhus/camelcase

camelcase: the Unicode aware string converter that is not idempotent

Convert a dash/dot/underscore/space separated string to camelCase: foo-bar → fooBar

699 stars105 forksJavaScriptMIT

At a glance

What is it?
camelcase turns dash, dot, underscore or space separated strings into camelCase or PascalCase, handles Unicode input correctly, and offers options for preserving consecutive capitals, for how digits are treated as word boundaries, and for locale specific case mapping. The README is explicit that the function is not idempotent in general, and that mixed case input is not idempotent either, because word boundaries have to be guessed.
Who is it for?
camelcase is worth using when you are converting identifiers that came from a wire format, a file name or a config key, because it handles the separators and the Unicode cases that a regular expression gets wrong, and the locale option is the part most home-grown versions lack, since the Turkish dotted I alone changes the output of an ordinary word.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 11 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One function, four separators, and the Unicode cases that break regexes

camelcase converts a dash, dot, underscore or space separated string into camelCase or PascalCase, and the whole package is a single function with options.

The basic behaviour is what you would expect, and the examples in the README cover the separator variants and the array input:

js
import camelCase from 'camelcase';

camelCase('foo-bar');
//=> 'fooBar'

camelCase('foo_bar');
//=> 'fooBar'

camelCase('Foo-Bar');
//=> 'fooBar'

camelCase('розовый_пушистый_единорог');
//=> 'розовыйПушистыйЕдинорог'

That last example is the one that separates this from a typical hand-rolled version. Converting a Cyrillic string to camelCase requires understanding where a word starts, and a regular expression written with ASCII character classes will not do it. The README states the goal plainly: it correctly handles Unicode strings.

The input can also be an array of words, which is useful when the words are already known, and it accepts a string array whose entries are treated as already separated:

js
camelCase(['foo', 'bar']);
//=> 'fooBar'

camelCase(['__foo__', '--bar']);
//=> '__fooBar'

Note what happens in that second case. Leading separator characters on the first element are preserved, and the leading punctuation of the second element is dropped, which is a detail worth knowing if you are normalising identifiers that carry a prefix.

The package is ESM only, with type set to module and an exports map pointing at the types and the default entry, it declares no side effects, and it requires Node 22 or newer. It is MIT licensed, the author is Sindre Sorhus, and the newest release is 10.0.0 from 2026-10-12.

Four options, and the two that change the output most

The options object has four entries, and they are not equal in how often they matter.

pascalCase uppercases the first character, so foo-bar becomes FooBar, and the default is false. preserveConsecutiveUppercase keeps a run of capitals together, so foo-BAR becomes fooBAR rather than fooBar, and its default is also false. Between them they cover the two common conventions: one where an acronym in the source is treated as one word, and one where it is expanded.

capitalizeAfterNumber is the one with the most thought behind it, and it defaults to true. It controls whether a letter immediately following digits is treated as a word boundary. When it is on, the first letter after a numeric sequence is capitalised, unless that letter is itself followed by a separator, which is similar to Lodash but without the separator condition.

That extra condition exists to protect tokens like b2b, so b2b_registration becomes b2bRegistration and not b2BRegistration. It also means a trailing separator stops the capitalisation, so 1a becomes 1A while 1a. becomes 1a.

js
import camelCase from 'camelcase';

camelCase('foo2bar', {capitalizeAfterNumber: true});
//=> 'foo2Bar'  (capitalizes 'b' after digit)

camelCase('foo2bar', {capitalizeAfterNumber: false});
//=> 'foo2bar'  (preserves lowercase 'b')

Turning the option off means digits are not word boundaries at all, the original case of the letters after them is preserved, and separators still create boundaries. The README attributes that behaviour to the Google Java Style Guide, which treats numbers as non-boundary characters.

The examples with the option off show why it matters in practice. Textures_3d becomes textures3d while Textures_3D becomes textures3D, and foo_2_bar becomes foo2Bar because there the bar follows a separator rather than a digit.

Locale decides the case mapping, and the Turkish dotted I is the test case

The locale option is the one that quietly changes the answer on a machine whose settings you may not control, and the examples are built around the Turkish locale for a reason.

The locale parameter indicates which locale to use to convert to upper or lower case according to locale specific case mappings, and if several locales are given in an array the best available one is used. The default is the host environment's current locale.

js
import camelCase from 'camelcase';

camelCase('lorem-ipsum', {locale: 'en-US'});
//=> 'loremIpsum'

camelCase('lorem-ipsum', {locale: 'tr-TR'});
//=> 'loremİpsum'

The difference is the dotted capital I, which is the correct uppercase of i in Turkish, so the same input produces a different identifier depending on the locale. In English you get loremIpsum and in Turkish you get loremİpsum, and a CI runner with a Turkish locale produces a different key than a developer laptop.

The array form is a fallback chain, so locale: ['en-US', 'en-GB'] and locale: ['tr', 'TR', 'tr-TR'] both work as intended, the first picking the English mapping and the second the Turkish one.

There is also a way out of the locale dependence entirely. Setting locale: false ignores the platform locale and uses the Unicode Default Case Conversion algorithm instead, which is locale independent. The README demonstrates the difference on a platform with tr-TR set: the default call gives loremİpsum and the locale: false call gives loremIpsum.

For a library that turns strings into identifiers, that is a real design decision rather than a detail, because an identifier that changes with the machine's settings is a bug waiting to happen in a build pipeline.

Not idempotent, and the README says so with examples

The most useful thing in the README is the note explaining that the function is not idempotent in general.

The first example is that camelCase of the string { A gives {A, while camelCase of {A gives {a. Applying the function to its own output changes the result, because the first pass left a capital A that the second pass lowercases and then treats as a word start.

The second is about mixed case input. The README says mixed case input is not idempotent either, because the word boundaries have to be guessed, and gives camelCase of Ac_c A producing acCA while camelCase of acCA produces acCa.

The reason is the same in both cases and it is inherent to the design. The function has to infer where one word ends and the next begins from the separators and from the case of the letters, and once it has lowercased and rejoined the words, that information is gone. A second pass has to guess again, and the guess can differ.

What this means in practice is that camelCase is a one-way transformation, not a normal form. If you camelCase a value and store it, do not camelCase it again later, and if you are comparing two strings for equality, compare the converted forms rather than expecting the conversion to be stable. A pipeline that runs a normalisation step twice, for example a build step and then a test fixture generator, is exactly where this shows up as a confusing mismatch.

The same guessing problem is why the preserveConsecutiveUppercase option exists. With it off, foo-BAR is treated as foo, bar, so the capitals do not tell you that BAR was one word. With it on, that information is used as a boundary signal instead.

A small package with a strict toolchain around it

The repository is minimal in the way a focused utility package should be. The top-level entries are index.js, index.d.ts, index.test-d.ts for the type tests, test.js, package.json, readme.md, the licence and the usual .editorconfig, .gitattributes, .gitignore and .npmrc files, plus a .github/ directory.

The package.json is worth a look for what it says about the constraints. The type is module, the exports map has a types entry and a default entry, sideEffects is false so a bundler can drop the import entirely when it is unused, and the engines field requires Node 22 or newer. The files list contains only index.js and index.d.ts, so the test file and the type test do not ship.

The test script is three tools in one line: xo for linting, node --test for the test run, and nz. The devDependencies show xo at version 5 and nz, plus a typescript alias under the @typescript/native name pointing at typescript 7.

The version history shows the pace of a package with a large user base. v8.0.0 came in August 2023, v9.0.0 in November 2025, and v10.0.0 on 2026-10-12, with the last push on the same day. Major version bumps every couple of years suggest breaking changes, most likely the engine requirement and the module format, and the CHANGELOG is where the specifics live.

The related projects list is a small ecosystem of one-liners from the same author. decamelize is the inverse of this module, titleize capitalises every word, humanize-string converts a camelised, dasherised or separator joined string into a humanised one, and camelcase-keys does the same conversion to object keys. The last one is the one you will actually want, because converting the keys of an object is a different problem from converting one string.

Editorial conclusion

camelcase is worth using when you are converting identifiers that came from a wire format, a file name or a config key, because it handles the separators and the Unicode cases that a regular expression gets wrong, and the locale option is the part most home-grown versions lack, since the Turkish dotted I alone changes the output of an ordinary word. It is the wrong tool when you need a round trip, because the function is not idempotent and applying it twice can give you a different string, so a value you camelCase and store will not survive being camelCased again. Before you depend on it, verify three things yourself: that your input does not mix case in a way that changes the boundary guess, since camelCase of Ac_c A and of acCA differ, that the locale default is what you want on a machine whose locale is not English, because the default is the host environment's current locale, and that you do not pass untrusted user input without a length limit, which the README explicitly warns about.

Frequently asked questions

What does the camelcase JavaScript package do?

It converts a dash, dot or space separated string to camelCase or PascalCase, and it handles Unicode strings correctly, so a Cyrillic phrase becomes a camelCased Cyrillic identifier. The input can be a string or an array of strings.

Is camelcase idempotent?

No, and the README states this explicitly. camelCase of { A gives {A while camelCase of {A gives {a, and mixed case input is not idempotent either because the word boundaries have to be guessed: camelCase of Ac_c A gives acCA while camelCase of acCA gives acCa.

What does the capitalizeAfterNumber option do?

It controls whether a letter immediately following digits is treated as a word boundary. When true, the first letter after a numeric sequence is capitalised unless that letter is itself followed by a separator, which keeps a token like b2b intact so b2b_registration becomes b2bRegistration. When false, digits are not boundaries and the original case after them is preserved, following the Google Java Style Guide.

Why does camelcase produce different output on different machines?

The default locale is the host environment's current locale, and case mapping is locale specific, so a machine with a Turkish locale turns lorem-ipsum into loremİpsum with a dotted capital I while an English machine gives loremIpsum. Setting locale to false makes the conversion use the locale independent Unicode Default Case Conversion algorithm instead.

How do I install camelcase and what are the requirements?

Run npm install camelcase. The package is ESM only, declares no side effects, requires Node 22 or newer, and its newest release is 10.0.0 from 2026-10-12. The README also warns that if you use it on untrusted user input you should limit the length to something reasonable.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. sindresorhus/camelcase on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/sindresorhus-camelcase.svg)](https://hysenlabs.com/projects/sindresorhus-camelcase)

Was this useful?