The Rules Behind Title Case, Sentence Case and camelCase

Changing text to upper or lower case is one line of JavaScript. Changing “a guide to the iPhone: what to look for” into a proper headline is not. You have to know that “to” and “the” stay small, that “For” at the end gets a capital, and that “iPhone” must not become “Iphone”. When I built ToolOnTap’s case converter, most of the work went into rules like these.

This article explains those rules as the code applies them, including the places where it has to guess.

Title Case: which small words stay small

Style guides agree that a title capitalizes its main words and leaves some short words lowercase. They disagree on which short words. The converter follows the AP rule: articles, conjunctions and prepositions of three letters or fewer stay lowercase. That list in the code is:

a, an, the, and, but, or, nor, for, so, yet, as, at, by, in, of, off, on, per, to, up, via, vs, v

So “with” and “from” get a capital, because they have four letters. Chicago style would lowercase them too, so a headline that follows Chicago may need two small edits.

A small word still gets a capital in three places:

  1. At the start of the line. “The Lord of the Rings”.
  2. After a colon, a dash or the end of a sentence. “A Guide to the iPhone: What to Look For”. The “What” is capitalized because it follows the colon.
  3. At the end of the line. “What Are You Looking At”. The last word of a title is always capitalized, even “at”.

Hyphenated words are handled part by part, so “state-of-the-art tools” becomes “State-of-the-Art Tools”. The small words inside the compound stay small, and the dash only counts as a break when there’s a space before it, as in “Web — A Guide”.

There’s one case the rule can’t see. In “how to set up your account”, “up” is part of the verb “set up”, and AP would capitalize it. The converter only knows that “up” is short, so it gives “How to Set up Your Account”. Telling a preposition from part of a phrasal verb needs grammar, not a word list, and I’d rather leave that one to you than guess wrong elsewhere.

Words you capitalized on purpose

The easiest way to write title case is to lowercase everything, then capitalize. That breaks every brand name and acronym: “iPhone” becomes “Iphone”, “NASA” becomes “Nasa”. So before changing a word, the converter checks whether its capitals look deliberate:

const deliberate = (w: string) => hasUpper(w.slice(1)) && (hasLower(w) || [...w].filter(c => LETTER.test(c)).length > 1)

A word is left alone if it has a capital letter after the first character, and it either also has lowercase letters (iPhone, McDonald) or has more than one letter (NASA, PDF). The second condition is there for “I” and “A”: a single capital letter isn’t a sign of anything, so those still follow the normal rules.

This works the same way in Sentence case. “my favorite phone is the iPhone. i’m sure” becomes “My favorite phone is the iPhone. I’m sure”. The first word of each sentence gets a capital, a lone “i” or “i’m” becomes “I” or “I’m”, and iPhone is left as it was. A new sentence starts after ., !, ?, the Arabic question mark ؟, an ellipsis, or a new line.

The limit is the other direction. If you typed “pdf” in lowercase, nothing tells the converter it’s an acronym, so Title Case gives “Pdf”. It can keep capitals you added; it can’t add the ones you left out.

Rescuing a line typed in caps lock

Deliberate capitals cause a problem with text typed in caps lock. In “WHY MY NASA PDF WONT OPEN”, every word has capitals after the first letter, so every word looks deliberate and nothing would change. So each line is checked first:

function shouting(text: string): boolean {
  // A single cased word is an acronym, not caps lock: "PDF" in an Arabic sentence stays PDF
  if (text.split(/\s+/).filter(w => [...w].some(c => hasUpper(c) || hasLower(c))).length < 2) return false
  const cased = [...text].filter(c => hasUpper(c) || hasLower(c))
  return cased.length > 0 && cased.filter(c => !hasLower(c)).length / cased.length > 0.6
}

If more than 60% of the letters that have a case are capitals, the line is treated as caps lock and lowercased before any other rule runs. The caps-lock line above becomes “Why My Nasa Pdf Wont Open”. The acronyms are lost, and that’s the honest trade-off: in a line that’s all capitals, there’s no way to tell NASA from WONT.

The 60% matters for normal text with one shouted word. “Hello WORLD from cairo” has 6 capitals out of 19 letters, about 32%, so Title Case keeps WORLD and gives “Hello WORLD From Cairo”.

Letters without case, such as Arabic, Chinese or digits, don’t count either way, and they pass through unchanged. Mixed text needed one more rule. In “حملت ملف PDF من الموقع”, the only letters with a case are P, D and F, all capitals, so the 60% test alone would call the line caps lock and turn PDF into “Pdf”. That’s why the function first checks how many words have cased letters at all: with only one, it’s an acronym, and the line is left as it is.

Code cases: finding the words first

camelCase, PascalCase, snake_case, kebab-case and CONSTANT_CASE are all the same list of words, joined differently. The hard part is getting the list, because identifiers hide their word boundaries. splitWords does it in three steps:

export function splitWords(line: string): string[] {
  return line
    .replace(/(\p{Ll}|\p{N})(\p{Lu})/gu, '$1 $2')
    .replace(/(\p{Lu})(\p{Lu}\p{Ll})/gu, '$1 $2')
    .split(/[^\p{L}\p{M}\p{N}]+/u)
    .filter(Boolean)
}
  1. Put a space where a lowercase letter or digit meets a capital: getHTMLPage becomes get HTMLPage.
  2. Put a space before the last capital of a run when a lowercase letter follows it: HTMLPage becomes HTML Page. This keeps acronyms together.
  3. Split at anything that isn’t a letter, mark or digit: spaces, underscores, hyphens, brackets.

The words are then lowercased and joined in the chosen style. Some results:

Input camelCase snake_case CONSTANT_CASE
XMLHttpRequest xmlHttpRequest xml_http_request XML_HTTP_REQUEST
getHTMLPage getHtmlPage get_html_page GET_HTML_PAGE
userID userId user_id USER_ID
user_id-2 userId2 user_id_2 USER_ID_2
Order ID (new) orderIdNew order_id_new ORDER_ID_NEW

\p{Lu} and \p{Ll} are Unicode’s uppercase and lowercase letter classes, so accented letters like É and é split the same way as plain ones.

Line by line

All of the code styles work on each line separately. Paste a column of thirty field names from a database export, pick camelCase, and you get thirty property names back, one per line. Without that, the whole list would join into one very long identifier. Title Case and Sentence case work per line too, which is why the caps-lock check runs per line: one shouted line in a document doesn’t cause the rest to be lowercased.

One honest note about languages. JavaScript’s toUpperCase and toLowerCase use the general Unicode rules, not a language’s own. In Turkish, i and ı are different letters, and the general rules don’t know that, so check dotted and dotless i after converting Turkish text.

You can try every rule above in the case converter, and the other text tools are one click away.

Tools in this article

  • Case converter

    Convert text to sentence case, Title Case, UPPERCASE or lowercase, or to camelCase, snake_case and kebab-case for code. Free, in your browser.

More articles