Skip to main content

Command Palette

Search for a command to run...

Building a Text Case Converter in React — camelCase, PascalCase, snake_case, kebab-case, and More

How to implement 8 text case transformations with real-time conversion, copy-to-clipboard, and correct tokenization for mixed-input strings

Updated
4 min read
Building a Text Case Converter in React — camelCase, PascalCase, snake_case, kebab-case, and More

A case converter sounds trivial until you think about the input: users paste myVariableName, MY_CONSTANT, some-slug-text, or Title Case Words and expect correct output regardless. Here's how the Case Converter handles it.

The Core Problem: Tokenization

Every case transformation comes down to one operation: split the input into words, then rejoin in the target format. The tricky part is splitting correctly from any input format.

function tokenize(input: string): string[] {
  return input
    // Insert a space before transitions from lowercase/digit to uppercase (camelCase → camel Case)
    .replace(/([a-z\d])([A-Z])/g, '\(1 \)2')
    // Insert a space before transitions from multiple uppercase to uppercase+lowercase (HTMLParser → HTML Parser)
    .replace(/([A-Z]+)([A-Z][a-z])/g, '\(1 \)2')
    // Replace hyphens, underscores, and dots with spaces
    .replace(/[-_.\s]+/g, ' ')
    // Trim and split on whitespace
    .trim()
    .split(/\s+/)
    .filter(Boolean)
    .map(w => w.toLowerCase());
}

This handles all common input formats:

  • myVariableName['my', 'variable', 'name']

  • MY_CONSTANT['my', 'constant']

  • some-slug-text['some', 'slug', 'text']

  • Title Case Words['title', 'case', 'words']

  • HTMLParser['html', 'parser']

The 8 Transformations

With clean tokens, the transformations are straightforward:

function capitalize(word: string): string {
  return word.charAt(0).toUpperCase() + word.slice(1);
}

const converters: Record<string, (tokens: string[]) => string> = {
  camelCase:          t => t[0] + t.slice(1).map(capitalize).join(''),
  PascalCase:         t => t.map(capitalize).join(''),
  snake_case:         t => t.join('_'),
  kebab_case:         t => t.join('-'),
  SCREAMING_SNAKE:    t => t.join('_').toUpperCase(),
  'Title Case':       t => t.map(capitalize).join(' '),
  'Sentence case':    t => capitalize(t.join(' ')),
  lowercase:          t => t.join(' '),
  UPPERCASE:          t => t.join(' ').toUpperCase(),
};

Converting any input to any format:

function convert(input: string, format: string): string {
  const tokens = tokenize(input);
  if (tokens.length === 0) return '';
  return converters[format](tokens);
}

State

const [input, setInput]   = useState('');
const [output, setOutput] = useState<Record<string, string>>({});

useEffect(() => {
  if (!input.trim()) {
    setOutput({});
    return;
  }
  const tokens = tokenize(input);
  const results: Record<string, string> = {};
  for (const [format, fn] of Object.entries(converters)) {
    results[format] = fn(tokens);
  }
  setOutput(results);
}, [input]);

All 8 formats update in real-time as the user types. Running all conversions on every keystroke is cheap — tokenization is O(n) on input length and the transforms are O(tokens).

Copy to Clipboard

Each output row has a Copy button. Feedback is a 1.5-second "Copied!" state per row:

const [copied, setCopied] = useState<string | null>(null);

async function copyToClipboard(format: string, value: string) {
  try {
    await navigator.clipboard.writeText(value);
    setCopied(format);
    setTimeout(() => setCopied(null), 1500);
  } catch {
    // Fallback for browsers without clipboard API
    const el = document.createElement('textarea');
    el.value = value;
    document.body.appendChild(el);
    el.select();
    document.execCommand('copy');
    document.body.removeChild(el);
    setCopied(format);
    setTimeout(() => setCopied(null), 1500);
  }
}
{Object.entries(output).map(([format, value]) => (
  <div key={format} className="flex items-center gap-3 p-3 rounded-lg bg-zinc-50 dark:bg-zinc-900">
    <span className="w-36 text-xs font-mono text-zinc-500">{format}</span>
    <span className="flex-1 font-mono text-sm">{value}</span>
    <button
      onClick={() => copyToClipboard(format, value)}
      className="text-xs px-3 py-1 rounded bg-zinc-200 hover:bg-zinc-300"
    >
      {copied === format ? 'Copied!' : 'Copy'}
    </button>
  </div>
))}

Edge Cases

Empty inputtokenize('') returns []. The output panel shows nothing.

Single character'a' → tokens ['a']. All transforms produce 'a', 'A', etc. Correct.

Numbers'version2release'['version', '2release']. Numbers don't get split from adjacent lowercase. Good enough for most identifiers.

Acronyms'parseHTML' → the regex ([a-z\d])([A-Z]) catches the e→H transition: 'parse HTML'. Then ([A-Z]+)([A-Z][a-z]) catches nothing here since HTML is all-caps. Tokens: ['parse', 'html']. PascalCase output: 'ParseHtml' — loses the all-caps. This is a known limitation.

If acronym preservation matters, you'd need a dictionary of known acronyms. For a general-purpose converter, the simpler approach is correct for 99% of inputs.

UnicodecharAt(0).toUpperCase() works for ASCII. For Unicode strings (über, ñoño), use:

function capitalize(word: string): string {
  return word.slice(0, 1).toUpperCase() + word.slice(1);
}

String.prototype.slice is Unicode-safe for most Latin-extended characters. Full Unicode support would require Intl.Segmenter.

The "Live" Format Detection

One useful UX addition: detect what format the input is already in and highlight it:

function detectFormat(input: string): string | null {
  if (/^[a-z][a-zA-Z0-9]*$/.test(input) && /[A-Z]/.test(input)) return 'camelCase';
  if (/^[A-Z][a-zA-Z0-9]*$/.test(input) && /[a-z]/.test(input)) return 'PascalCase';
  if (/^[a-z0-9]+(_[a-z0-9]+)+$/.test(input)) return 'snake_case';
  if (/^[a-z0-9]+(-[a-z0-9]+)+$/.test(input)) return 'kebab_case';
  if (/^[A-Z0-9]+(_[A-Z0-9]+)+$/.test(input)) return 'SCREAMING_SNAKE';
  return null;
}

Show a subtle badge on the row that matches the detected format so the user knows what they pasted.

Putting It Together

The whole converter is ~120 lines: a textarea, a useEffect running all conversions on input change, and a list of output rows with copy buttons. No library needed — pure string manipulation.

Try it: Case Converter → ultimatetools.io

More from this blog

U

Ultimate Tools — Developer Blog

129 posts