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
| Char | Name | Code point | HTML entity | Use for |
|---|---|---|---|---|
| – | En dash | U+2013 | – | Ranges: 2020–2026, pages 10–15 |
| — | Em dash | U+2014 | — | Parenthetical breaks — like this one |
| - | Hyphen-minus | U+002D | — | Compound words, the keyboard key |
| − | Minus sign | U+2212 | − | Actual subtraction; aligns with digits |
| … | Ellipsis | U+2026 | … | Omission — better spacing than three dots |
| • | Bullet | U+2022 | • | List markers in plain text |
| · | Middle dot | U+00B7 | · | Separating inline items |
| § | Section sign | U+00A7 | § | Legal references |
| ¶ | Pilcrow | U+00B6 | ¶ | Paragraph marks |
| † | Dagger | U+2020 | † | Footnotes |
| ‰ | Per mille | U+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
| Char | Name | Code point | Entity |
|---|---|---|---|
| " | Left double quotation | U+201C | “ |
| " | Right double quotation | U+201D | ” |
| ' | Left single quotation | U+2018 | ‘ |
| ' | Right single / apostrophe | U+2019 | ’ |
| " | Straight double quote | U+0022 | " |
| ' | Straight apostrophe | U+0027 | ' |
| « » | Guillemets | U+00AB U+00BB | « » |
| „ " | German quotes | U+201E U+201C | „ |
| ′ ″ | Prime, double prime | U+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
| Char | Name | Code point | Entity |
|---|---|---|---|
| → | Right arrow | U+2192 | → |
| ← | Left arrow | U+2190 | ← |
| ↑ ↓ | Up, down arrow | U+2191 U+2193 | ↑ ↓ |
| ↔ | Left-right arrow | U+2194 | ↔ |
| ⇒ | Double right arrow | U+21D2 | ⇒ |
| × | Multiplication | U+00D7 | × |
| ÷ | Division | U+00F7 | ÷ |
| ± | Plus-minus | U+00B1 | ± |
| ≈ | Approximately equal | U+2248 | ≈ |
| ≠ | Not equal | U+2260 | ≠ |
| ≤ ≥ | Less/greater or equal | U+2264 U+2265 | ≤ ≥ |
| ∞ | Infinity | U+221E | ∞ |
| √ | Square root | U+221A | √ |
| ° | Degree | U+00B0 | ° |
| µ | Micro sign | U+00B5 | µ |
| ½ ¼ ¾ | Fractions | U+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.
Currency and legal marks
| Char | Name | Code point | Entity |
|---|---|---|---|
| € | Euro | U+20AC | € |
| £ | Pound sterling | U+00A3 | £ |
| ¥ | Yen / yuan | U+00A5 | ¥ |
| ₹ | Indian rupee | U+20B9 | ₹ |
| ¢ | Cent | U+00A2 | ¢ |
| ₽ | Russian ruble | U+20BD | ₽ |
| © | Copyright | U+00A9 | © |
| ® | Registered trademark | U+00AE | ® |
| ™ | Trademark | U+2122 | ™ |
| ✓ ✗ | Check, ballot X | U+2713 U+2717 | ✓ |
| ★ ☆ | Star filled, outline | U+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 point | Name | What it does |
|---|---|---|
U+00A0 | Non-breaking space | Looks like a space, is not one. Fails split(' ') and trim() in some languages |
U+200B | Zero-width space | No width at all. Breaks string equality invisibly |
U+200C | Zero-width non-joiner | Prevents ligatures |
U+200D | Zero-width joiner | Combines emoji into compound sequences |
U+FEFF | Byte order mark | Appears at file start; corrupts JSON and CSV parsing |
U+202E | Right-to-left override | Reverses displayed text — used to disguise filenames |
U+2060 | Word joiner | Prevents a line break, no width |
U+00AD | Soft hyphen | Invisible 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.
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.
| Form | Does | Use for |
|---|---|---|
| NFC | Composes into precomposed characters | The default. Storage, transmission, comparison |
| NFD | Decomposes into base plus combining marks | Stripping accents, some sorting |
| NFKC | NFC plus compatibility folding | Search indexing — turns fi into fi, ① into 1 |
| NFKD | NFD plus compatibility folding | Aggressive 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:
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:
| Range | UTF-8 bytes | Covers |
|---|---|---|
| U+0000 – U+007F | 1 | ASCII — identical bytes to ASCII |
| U+0080 – U+07FF | 2 | Latin accents, Greek, Cyrillic, Hebrew, Arabic |
| U+0800 – U+FFFF | 3 | Most CJK, Devanagari, symbols |
| U+10000 – U+10FFFF | 4 | Emoji, 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
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.
Entering characters
| Platform | Method |
|---|---|
| Windows | Win + . opens the emoji and symbol picker. Or Alt + the decimal code on the numeric keypad. |
| macOS | Ctrl + Cmd + Space opens Character Viewer. Opt + - gives an en dash, Shift + Opt + - an em dash. |
| Linux | Ctrl + Shift + U, then the hex code, then Enter. |
| HTML | — or — or — |
| CSS | content: "\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
.lengthfor 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.