Unicode Normalization Explained: NFC, NFD, NFKC, and NFKD With Practical Examples
UnicodeText ProcessingJavaScriptPythonDatabases

Unicode Normalization Explained: NFC, NFD, NFKC, and NFKD With Practical Examples

UUnicode.live Editorial Team
2026-11-02
6 min read

Compare NFC, NFD, NFKC, and NFKD, with practical code examples for normalizing Unicode safely across apps and databases.

Unicode normalization makes text that looks the same behave consistently in search, validation, storage, and APIs. This practical guide compares NFC, NFD, NFKC, and NFKD, shows why visually identical strings can have different bytes, and provides JavaScript, Python, PHP, and database-oriented techniques for choosing and testing a normalization strategy.

Overview

Unicode allows more than one valid sequence of code points to represent what readers perceive as the same text. A common example is “é”. It can be stored as a single precomposed code point, U+00E9 LATIN SMALL LETTER E WITH ACUTE, or as two code points: U+0065 LATIN SMALL LETTER E followed by U+0301 COMBINING ACUTE ACCENT. The displayed result may be identical, while the underlying code points and UTF-8 bytes differ.

This is a Unicode equivalence problem, not necessarily an encoding error. Normalization transforms text into a predictable form so that operations such as comparison, deduplication, indexing, and validation are less likely to produce surprising results. It does not repair every text problem: mojibake, invalid byte decoding, confusable characters, transliteration, and language-specific case handling require separate decisions.

The four standard normalization forms are divided into two groups:

  • NFC applies canonical decomposition and then recomposes where possible.
  • NFD applies canonical decomposition without recomposition.
  • NFKC applies compatibility decomposition and then recomposes where possible.
  • NFKD applies compatibility decomposition without recomposition.

Canonical forms preserve distinctions that Unicode treats as meaningful. Compatibility forms may intentionally reduce distinctions between characters with different visual or semantic roles. That makes NFKC and NFKD useful in selected search and identifier workflows, but potentially inappropriate for preserving user-authored text exactly.

For related debugging work, see the Unicode encoding errors checklist and the guide to finding code points and UTF-8 bytes.

How to compare options

Choose a normalization form by first deciding what the text represents. There is no universally correct form for every field.

  1. Identify the operation. Are you preserving a document, comparing names, building a search key, validating an identifier, or generating a URL slug? Display text and comparison keys often need different treatment.
  2. Decide whether compatibility distinctions matter. Characters such as fullwidth Latin letters, circled numbers, or some presentation variants may be intentionally distinct in a document. NFKC and NFKD can collapse some of these distinctions.
  3. Check the system boundary. Normalize at a defined point, such as input validation, before indexing, or while creating a comparison key. Avoid having different services silently apply different forms.
  4. Test real input. Include accented characters, combining marks, non-Latin scripts, punctuation, emoji sequences, and characters used by your application. Compare both code points and rendered output.
  5. Document reversibility expectations. NFC and NFD can change representation while retaining canonical distinctions. Compatibility normalization can lose distinctions, so it should not replace the original text when exact preservation matters.

A useful pattern is to store the original user-facing value and create a separately named normalized or search value. This allows consistent matching without silently changing what the user entered. Normalization is also distinct from case folding. If matching should ignore case, define a case-handling step separately and test its behavior for the languages your application supports.

Feature-by-feature breakdown

NFC: the common canonical form

NFC composes canonically equivalent sequences where a composed representation exists. It is often a practical default for general text interchange when you want a stable canonical representation while preserving canonical distinctions. NFC does not turn arbitrary text into ASCII, remove accents, or resolve visually similar characters from different scripts.

const composed = "e\u0301";
const nfc = composed.normalize("NFC");
console.log(nfc); // é
console.log(nfc === "é"); // true

NFD: explicit canonical decomposition

NFD separates canonically decomposable characters into their component code points. This can be useful for inspecting combining marks or implementing carefully designed accent-related processing. It should not be used as a generic “remove accents” operation without understanding the script and the marks involved.

const nfd = "é".normalize("NFD");
console.log([...nfd].map(ch => ch.codePointAt(0).toString(16));
// Often: ["65", "301"]

NFKC and NFKD: compatibility forms

NFKC and NFKD apply compatibility decomposition. They are appropriate when a workflow intentionally treats certain presentation variants as equivalent, such as creating a comparison key for user-entered identifiers or search. They can also remove distinctions that matter in scholarly, legal, mathematical, or typographic content. Keep the original value if that distinction may need to be displayed or recovered.

const value = "ABC"; // fullwidth Latin letters
console.log(value.normalize("NFKC")); // ABC

In Python, use the standard library:

import unicodedata

text = "e\u0301"
print(unicodedata.normalize("NFC", text))
print(unicodedata.normalize("NFD", text))
print(unicodedata.normalize("NFKC", text))
print(unicodedata.normalize("NFKD", text))

In PHP, the Normalizer class from the internationalization extension provides the same named forms:

$text = "e\u{0301}";
$normalized = Normalizer::normalize($text, Normalizer::FORM_C);

if ($normalized === false) {
    throw new RuntimeException("Normalization failed");
}

For databases, do not assume that a text column, collation, or index automatically performs Unicode normalization. Database equality and sorting rules vary, and a case-insensitive collation is not the same as normalization. If normalized comparison is important, normalize before persistence or maintain an explicit normalized key, then verify the behavior with the database and driver used in production.

Best fit by scenario

  • General text storage or interchange: NFC is often a reasonable canonical target, provided your application preserves the original meaning and tests its supported scripts.
  • Combining-mark inspection: Use NFD when you specifically need decomposed code points for analysis. Treat mark removal as a separate, language-aware transformation.
  • Search and loose matching: Consider a dedicated comparison key using a documented form, potentially NFKC, plus separately defined case and punctuation rules. Keep the display value unchanged.
  • Usernames or machine identifiers: Define an allowed-character policy first. Normalization alone does not prevent confusables, mixed scripts, or security-sensitive lookalikes. See the Unicode confusables guide.
  • URLs and API payloads: Normalize at the application boundary only when the API contract calls for it. Then test serialization, URL encoding, and decoding as separate steps using the URL encoding guide.
  • Exact documents and rich text: Prefer preserving the original text. Apply a normalized copy for search or comparison rather than replacing the source.

When debugging a mismatch, inspect the string at three levels: rendered characters, Unicode code points, and encoded bytes. A browser-based Unicode utility can make those differences visible, but the final behavior should also be confirmed in the same language runtime, database, or API stack that handles the production data.

When to revisit

Revisit your normalization policy when you add a new language or script, change database engines or collations, introduce a search provider, modify identifier rules, or move text through a new API or serialization layer. Unicode data and runtime behavior can also change as platforms and libraries are upgraded, so normalization should be part of regression testing rather than a one-time cleanup.

Make the review practical:

  1. List every text field that is displayed, searched, compared, indexed, or used as an identifier.
  2. Record the chosen normalization form for each field and whether the original value is retained.
  3. Add test pairs such as precomposed and decomposed accented text, compatibility variants, combining marks, non-Latin names, and emoji sequences.
  4. Inspect code points and UTF-8 bytes when a visual comparison is misleading.
  5. Verify behavior end to end: browser input, JavaScript, server language, database storage, indexing, API serialization, and output.
  6. Review security-sensitive identifiers separately for confusables and mixed-script risks.

For a broader release process, combine this work with a Unicode text QA checklist. The key rule is simple: normalize deliberately, preserve original text when fidelity matters, and never treat visually identical strings as proof of identical data.

Related Topics

#Unicode#Text Processing#JavaScript#Python#Databases
U

Unicode.live Editorial Team

Technical Editor

Senior editor and content strategist. Writing about technology, design, and the future of digital media. Follow along for deep dives into the industry's moving parts.