Unicode Character Reference: The Symbols You Actually Need

Unicode contains more than 150,000 characters, and you will use perhaps sixty of them. This page collects the ones that come up in real work — the correct dashes, the proper quotation marks, arrows, mathematical operators and currency symbols — with code points and HTML entities. It also covers the invisible characters that silently corrupt data, which is the part most references leave out.

The distinction that matters

Unicode assigns each character a number: U+2014 is an em dash. UTF-8 is how that number becomes bytes. Nearly every "weird characters" bug is a mismatch in the second part — the right code points decoded with the wrong encoding.

Punctuation and dashes

CharNameCode pointHTML entityUse for
En dashU+2013–Ranges: 2020–2026, pages 10–15
Em dashU+2014—Parenthetical breaks — like this one
-Hyphen-minusU+002DCompound words, the keyboard key
Minus signU+2212−Actual subtraction; aligns with digits
EllipsisU+2026…Omission — better spacing than three dots
BulletU+2022•List markers in plain text
·Middle dotU+00B7·Separating inline items
§Section signU+00A7§Legal references
PilcrowU+00B6¶Paragraph marks
DaggerU+2020†Footnotes
Per milleU+2030‰Parts per thousand

Dashes: the one-line rule

Hyphen joins words (well-known). En dash spans a range (Monday–Friday) and is the width of an n. Em dash interrupts a sentence and is the width of an m. The minus sign is a fourth character again — it is drawn at the same height and width as digits, so −5 aligns in a column where -5 does not.

Quotation marks and apostrophes

CharNameCode pointEntity
"Left double quotationU+201C“
"Right double quotationU+201D”
'Left single quotationU+2018‘
'Right single / apostropheU+2019’
"Straight double quoteU+0022"
'Straight apostropheU+0027'
« »GuillemetsU+00AB U+00BB« »
„ "German quotesU+201E U+201C„
′ ″Prime, double primeU+2032 U+2033′

⚠️ The curly apostrophe in code

Word processors and messaging apps silently convert ' (U+0027) into ' (U+2019). Paste that into a code editor and you get SyntaxError: Invalid or unexpected token for a line that looks perfectly correct. The same substitution turns straight quotes into curly ones and breaks any string literal.

Never write code in a word processor. If a snippet from a document or chat refuses to run and you cannot see why, this is almost always the reason.

One typographic note: feet and inches use prime marks (5′ 10″), not quotation marks. Similarly, minutes and seconds of arc use prime and double prime. Using curly quotes there is a common and visible error.

Arrows, maths and symbols

CharNameCode pointEntity
Right arrowU+2192→
Left arrowU+2190←
↑ ↓Up, down arrowU+2191 U+2193↑ ↓
Left-right arrowU+2194↔
Double right arrowU+21D2⇒
×MultiplicationU+00D7×
÷DivisionU+00F7÷
±Plus-minusU+00B1±
Approximately equalU+2248≈
Not equalU+2260≠
≤ ≥Less/greater or equalU+2264 U+2265≤ ≥
InfinityU+221E∞
Square rootU+221A√
°DegreeU+00B0°
µMicro signU+00B5µ
½ ¼ ¾FractionsU+00BD U+00BC U+00BE½

Use × rather than the letter x for dimensions — 1920 × 1080 is correct and 1920 x 1080 is a typographic shortcut. The multiplication sign is also what search engines and screen readers interpret correctly.

CharNameCode pointEntity
EuroU+20AC€
£Pound sterlingU+00A3£
¥Yen / yuanU+00A5¥
Indian rupeeU+20B9₹
¢CentU+00A2¢
Russian rubleU+20BD₽
©CopyrightU+00A9©
®Registered trademarkU+00AE®
TrademarkU+2122™
✓ ✗Check, ballot XU+2713 U+2717✓
★ ☆Star filled, outlineU+2605 U+2606★

Invisible characters — the dangerous section

These occupy no visible space, survive copy-and-paste, and are effectively undetectable by eye. They cause bugs that look impossible.

Code pointNameWhat it does
U+00A0Non-breaking spaceLooks like a space, is not one. Fails split(' ') and trim() in some languages
U+200BZero-width spaceNo width at all. Breaks string equality invisibly
U+200CZero-width non-joinerPrevents ligatures
U+200DZero-width joinerCombines emoji into compound sequences
U+FEFFByte order markAppears at file start; corrupts JSON and CSV parsing
U+202ERight-to-left overrideReverses displayed text — used to disguise filenames
U+2060Word joinerPrevents a line break, no width
U+00ADSoft hyphenInvisible until a line breaks there

🚨 The BOM problem

Excel writes a UTF-8 byte order mark at the start of exported CSV files. It is invisible in every editor. But a parser reading that file sees the first column header as Name rather than Name — so row["Name"] returns nothing and only the first column is affected, which makes it look like a bug in your data rather than your encoding.

The same applies to JSON: JSON.parse throws on a leading BOM. Strip it explicitly when reading files you did not write.

// Strip a BOM text = text.replace(/^/, ''); // Find invisible characters in a suspect string [...str].forEach(c => { const cp = c.codePointAt(0); if (cp > 126 || cp < 32) { console.log(`U+${cp.toString(16).toUpperCase().padStart(4, '0')}`, JSON.stringify(c)); } }); // Remove zero-width characters entirely clean = str.replace(/[​-‍⁠]/g, ''); // Normalise non-breaking spaces to ordinary ones clean = str.replace(/ /g, ' ');

The non-breaking space deserves particular attention because it is so easy to produce accidentally: it is what you get when you paste from a web page, and in some editors it is what Alt+Space types. A configuration value with a trailing non-breaking space looks identical to one without and matches nothing.

Normalisation

Unicode allows some characters to be written more than one way, and the alternatives are not equal as strings even though they render identically.

const a = "café"; // e + U+0301 combining acute — 5 code points const b = "café"; // U+00E9 precomposed é — 4 code points a === b // false a.length // 5 b.length // 4 a.normalize('NFC') === b.normalize('NFC') // true
FormDoesUse for
NFCComposes into precomposed charactersThe default. Storage, transmission, comparison
NFDDecomposes into base plus combining marksStripping accents, some sorting
NFKCNFC plus compatibility foldingSearch indexing — turns fi into fi, ① into 1
NFKDNFD plus compatibility foldingAggressive normalisation for matching

⚠️ macOS stores filenames in NFD

Type a filename containing an accented character on macOS and the filesystem stores it decomposed. Linux and Windows store what you typed, usually NFC. So a file called café.pdf created on a Mac has a different byte sequence from the same name created on Linux — and a script matching filenames across the two silently finds nothing. Normalise both sides to NFC before comparing.

A practical use of NFD is stripping accents for slugs and search:

// "Crème Brûlée" → "Creme Brulee" str.normalize('NFD').replace(/[̀-ͯ]/g, ''); // Decompose, then delete the combining-mark block. // Works for most Latin scripts; not a general transliteration.

Code points, UTF-8 and string length

A code point is a number. An encoding turns it into bytes. UTF-8 uses one to four bytes depending on the value:

RangeUTF-8 bytesCovers
U+0000 – U+007F1ASCII — identical bytes to ASCII
U+0080 – U+07FF2Latin accents, Greek, Cyrillic, Hebrew, Arabic
U+0800 – U+FFFF3Most CJK, Devanagari, symbols
U+10000 – U+10FFFF4Emoji, rare scripts, historic characters

UTF-8's key design property is that ASCII is unchanged — any ASCII file is already valid UTF-8. That backward compatibility is why UTF-8 won over the alternatives and now accounts for the overwhelming majority of the web.

Why counting characters is harder than it looks

const s = "👨‍👩‍👧‍👦"; // one family emoji, visually s.length // 11 — UTF-16 code units [...s].length // 7 — code points // four person emoji joined by three U+200D zero-width joiners [...new Intl.Segmenter('en', { granularity: 'grapheme' }) .segment(s)].length // 1 — what a human sees

Three different answers, all correct for different questions. This matters for real features: a 280-character limit measured in UTF-16 units counts one family emoji as 11, while the user sees one character. Text truncation is worse — cutting a string mid-surrogate-pair produces an invalid character, and cutting between a base and its combining mark changes the letter.

✅ Use Intl.Segmenter for anything user-facing

Character counters, truncation with an ellipsis, and cursor movement should all operate on grapheme clusters — what a person would call a character. Intl.Segmenter is built into modern browsers and Node and requires no library. For byte limits, such as a database column, measure with TextEncoder instead.

// Byte length, for database and protocol limits new TextEncoder().encode(str).length; // "é" → 2 bytes, "中" → 3 bytes, "👍" → 4 bytes // A VARCHAR(255) in bytes holds far fewer than 255 CJK characters

Entering characters

PlatformMethod
WindowsWin + . opens the emoji and symbol picker. Or Alt + the decimal code on the numeric keypad.
macOSCtrl + Cmd + Space opens Character Viewer. Opt + - gives an en dash, Shift + Opt + - an em dash.
LinuxCtrl + Shift + U, then the hex code, then Enter.
HTML&mdash; or &#8212; or &#x2014;
CSScontent: "\2014" — hex, no U+
JavaScript"—", or "\u{1F44D}" for values above U+FFFF
Python"—" or "\N{EM DASH}"

Encoding text for a URL or transport?

Percent-encode and decode URLs, or convert to and from Base64 — instantly, in your browser, nothing uploaded.

Open the URL Encoder →

The working rules

  • Use UTF-8 everywhere — files, database columns, HTTP headers, HTML meta tags. Mixed encodings are the root of nearly every character bug.
  • Normalise to NFC before comparing or storing user-entered text.
  • Strip the BOM when reading CSV and JSON you did not produce.
  • Watch for invisible characters in pasted values — U+00A0 and U+200B are the usual suspects.
  • Count graphemes for users, bytes for databases, and never .length for either.
  • Never write code in a word processor. Curly quote substitution will break it.
  • Use the correct dash. Hyphen joins, en dash spans, em dash interrupts.

Frequently Asked Questions

What is the difference between Unicode and UTF-8?

Unicode is the catalogue: it assigns every character a number called a code point, such as U+0041 for 'A'. UTF-8 is one way of turning those numbers into bytes. The same code point can be stored as UTF-8, UTF-16 or UTF-32 — different byte sequences representing identical text. Unicode is the what; UTF-8 is the how.

Why does an emoji have a string length of 2?

Because JavaScript, Java and C# measure length in UTF-16 code units, not characters. Code points above U+FFFF are stored as a surrogate pair of two units, so '👍'.length is 2. Family emoji built from several joined characters can report 11 or more. Use Array.from(str).length or Intl.Segmenter to count what users would call characters.

What is a zero-width space and why is it a problem?

U+200B is a character that occupies no visible width. It is used legitimately for line-break hints in scripts without spaces, but it also survives copy-and-paste from web pages and documents. Pasted into a password field, a code editor or a data import, it produces values that look identical yet fail every comparison — one of the hardest bug classes to see.

What is Unicode normalisation?

Some characters can be written more than one way. The letter é is either the single code point U+00E9, or 'e' followed by a combining acute accent U+0301. They render identically and are not equal as strings. Normalisation converts text to a canonical form — NFC composes into single code points, NFD decomposes into base plus combining marks. Always normalise before comparing or storing user text.

Should I use HTML entities or the characters directly?

Use the characters directly in UTF-8 pages — they are more readable in source and behave identically. Entities remain necessary for the four characters with syntactic meaning in HTML (&, <, >, and " inside attributes), and are useful for invisible characters like   where a literal would be impossible to see in the source.

P

Written by Paras

We build free, browser-based file tools and write the reference material we wish existed when we were looking things up. Spotted an error? Tell us and we will fix it.