Unicode in CSS and JavaScript
Using Unicode in CSS and JavaScript comes down to three skills: writing escapes (\2192 in CSS, \u{2192} in JavaScript), counting characters correctly (emoji are two UTF-16 units), and normalizing text so é always matches é.
Unicode escapes in CSS
In CSS, a backslash followed by up to six hexadecimal digits is a character escape. It is most often used with the content property of ::before and ::after:
.next::after { content: " \2192"; } /* → */
.done::before { content: "\2713 "; } /* ✓ */
.price::before { content: "\20AC"; } /* € */
.quote::before { content: "\201C"; } /* “ */
An escape ends at the first character that is not a hex digit, or at a single space, which is swallowed. That catches people out when the next real character is itself a hex digit. "\B0C" is read as U+0B0C, not “°C”. Write "\B0 C", where the space ends the escape, or use the six-digit form "\0000B0C". Simpler still: save the stylesheet as UTF-8 and write content: "°C";.
Escapes also work in selectors and identifiers, for example to target a class that starts with a digit, but most projects never need that.
Generated content is read aloud
Screen readers announce text inserted with content. A decorative arrow becomes “right arrow” in the middle of a link. Modern browsers accept alternative text after a slash, content: "\2192" / "";, which keeps the arrow visible but silent. Where that syntax is not supported, keep meaningful words in the HTML and use generated symbols only for decoration.
Unicode escapes in JavaScript
JavaScript strings accept three kinds of escape:
"\xE9" // é two hex digits, U+0000–U+00FF only
"\u00E9" // é exactly four hex digits
"\u{1F600}" // 😀 braces allow any code point (ES2015+)
The four-digit form cannot reach characters above U+FFFF on its own, so older code writes emoji as a surrogate pair, "\uD83D\uDE00". Prefer the brace form in anything written today. To build characters from numbers, use String.fromCodePoint(0x1F600); to read them back, use str.codePointAt(0). The older fromCharCode and charCodeAt only handle single UTF-16 units and give wrong results for emoji.
Every SymbolHub symbol page lists the JavaScript, Python and CSS escape for its character in the code table, and the Unicode converter escapes whole strings.
Why "😀".length is 2
JavaScript strings are sequences of UTF-16 code units. Characters above U+FFFF, which includes most emoji, take two units, so length counts them twice. Three levels of “length” matter:
| Text | .length (UTF-16 units) | [...s].length (code points) | Grapheme clusters |
|---|---|---|---|
| é (precomposed) | 1 | 1 | 1 |
| 😀 | 2 | 1 | 1 |
| 👍🏽 (thumbs up + skin tone) | 4 | 2 | 1 |
| 👨👩👧 (family, joined by ZWJ) | 8 | 5 | 1 |
Spreading a string ([...s] or Array.from(s)) iterates by code point, which fixes the emoji case but still splits skin tones and zero width joiner sequences. To count what a reader sees, use Intl.Segmenter:
const seg = new Intl.Segmenter("en", { granularity: "grapheme" });
const count = [...seg.segment("👍🏽 ok")].length; // 4: 👍🏽, space, o, k
Use grapheme counts for character limits in forms, for truncating text with an ellipsis, and for reversing strings. Truncating by length can cut an emoji in half and leave a broken character.
Normalization: when é does not equal é
Some characters can be stored in more than one way. é can be the single code point U+00E9 or the letter e followed by the combining acute accent U+0301. Both look identical, but === says they are different, and so do naive searches and database lookups.
const a = "caf\u00E9"; // precomposed
const b = "cafe\u0301"; // decomposed
a === b; // false
a.normalize("NFC") === b.normalize("NFC"); // true
NFC (composed) is the right default for storing and comparing text; it is what most keyboards produce and what the web recommends. NFD (decomposed) is handy for stripping accents: normalize to NFD, then remove combining marks with /\p{M}/gu. The compatibility forms NFKC and NFKD go further and fold look-alikes, turning fi into “fi” and the bold mathematical letters used for fancy text back into plain letters, which is useful for search.
Regular expressions
Add the u flag whenever a pattern deals with non-ASCII text. It makes . match a whole code point, allows \u{…} escapes and enables Unicode property classes:
/\p{L}+/u // any letters, in any script
/\p{Sc}/u // any currency symbol: $ € £ ¥ ₹ ₿
/\p{Extended_Pictographic}/u // emoji-like pictographs
Property classes are far more reliable than hand-written ranges, which inevitably miss characters added in later Unicode versions.
Sorting, case and comparison
Plain sort() compares UTF-16 code units, which puts “Zebra” before “apple” and “é” after “z”. Use localeCompare or Intl.Collator for human ordering, and toLocaleUpperCase("tr") when case rules depend on the language (Turkish dotted and dotless i are the classic example). For case-insensitive matching, Intl.Collator with sensitivity: "base" treats “resume” and “résumé” as equal.
Bytes, URLs and storage
Length in bytes depends on the encoding. new TextEncoder().encode(s).length gives the UTF-8 byte count, which is what server limits and database columns usually measure. For URLs, encodeURIComponent("é") produces %C3%A9, the percent-encoded UTF-8 bytes. See UTF-8 explained for how those bytes are formed.
Developer checklist
- Save CSS and JavaScript files as UTF-8, and prefer literal characters over escapes when readability matters.
- Use
codePointAt,fromCodePointand[...s]instead of their UTF-16 equivalents. - Count, truncate and reverse by grapheme cluster with
Intl.Segmenter. - Normalize to NFC before comparing or storing user input.
- Give meaningful symbols a text alternative; hide decorative ones from screen readers.
Frequently asked questions
How do I add a Unicode symbol with CSS?
Use the content property on ::before or ::after. Either paste the character into a UTF-8 stylesheet, as in content: "→";, or write a backslash followed by its hex code point, 2192 for →.
Why is the length of an emoji 2 in JavaScript?
JavaScript strings count UTF-16 code units, and characters above U+FFFF, including most emoji, need two units (a surrogate pair). Use [...str].length for code points or Intl.Segmenter for visible characters.
How do I remove accents from a string in JavaScript?
Normalize the string to NFD so each accent becomes a separate combining mark, then delete the marks with a regular expression that matches the Unicode Mark property (p{M} with the u flag).
What is the difference between fromCharCode and fromCodePoint?
fromCharCode takes UTF-16 code units and cannot produce characters above U+FFFF from one number. fromCodePoint takes real code points, so String.fromCodePoint(0x1F600) returns 😀.