1. Presentation
  2. Fonts
  3. The details

Presentation · Fonts 09

The details

Readers rarely notice a curly apostrophe or a column of digits that lines up. They notice when those things are wrong, even if they cannot say why: the page looks typed rather than set. This page is about the small marks and alternate glyphs that make the difference, how CSS asks a font for them, and what happens when the font does not have them.

The earlier pages chose typefaces and loaded them. This one works at the level of single characters: which digits a table uses, whether an abbreviation shouts, which dash goes in a range, and why a font full of pictures turned out to be a bad idea.

1. Features live in the font

A font is more than one shape per character. Besides its default glyphs, an OpenType font can carry alternates: digits of equal width, small capitals, ligatures, fractions, a zero with a slash through it. Each set of alternates is a feature, named by a four-letter tag such as tnum (tabular numbers) or smcp (small caps), and stored as substitution rules in the font's GSUB table, the table that Web fonts and performance showed in the pipeline from character to glyph.

CSS cannot add a feature to a font. It can only ask for one, and the answer depends on the file. Some features are on by default, because the shaping engine applies them to all text: kerning (kern), standard ligatures (liga), contextual alternates (calt) and the features complex scripts need. The rest are off until a style turns them on. When a font lacks a requested feature, nothing happens, except in a few cases, small caps above all, where the browser fakes the result.

What this section's fonts can do

Before promising a feature in a design, check the font file, because one typeface can arrive with very different features. This section ships Inter and Literata twice: as Fontsource's Latin builds, which kept only a handful of features, and as cuts from the upstream fonts made with --layout-features="*", which kept them all. Subsetting a font, which Web fonts and performance recommended for speed, can silently remove features a design depends on.

OpenType features in this section's font files (GSUB; GPOS), from fonttools
FileBytesFeatures
Inter, Fontsource build72,920calt ccmp dnom frac locl numr pnum tnum; kern mark mkmk
Inter, all features91,388aalt calt case dlig dnom frac locl numr ordn pnum salt sinf subs sups tnum zero, ss01–ss08, cv01–cv13; cpsp kern
Literata, Fontsource build110,080ccmp dnom frac liga locl numr pnum rvrn tnum; kern mark
Literata, all features138,100aalt c2sc case ccmp dlig dnom frac liga lnum locl numr onum ordn pnum rvrn sinf smcp ss02 subs sups tnum zero; case cpsp kern
Bitter34,072calt ccmp dnom frac liga lnum locl numr pnum rvrn tnum; kern mark mkmk
Recursive128,708dnom frac locl numr pnum rvrn; kern (the full font adds case dlig ordn sups zero and thirteen stylistic sets)
Atkinson Hyperlegible Next33,996ccmp frac locl pnum tnum; kern mark mkmk

The feature-complete Inter costs 18 KB more than Fontsource's for a similar character set; the feature-complete Literata costs 28 KB more and covers fewer accented letters. Features cost bytes, so a production subset keeps the ones the design uses.

2. Numbers

Digits come in two independent pairs of styles, and a good text font offers all four combinations.

Lining and old-style figures

Lining figures all stand on the baseline and reach about the height of the capitals, like a row of soldiers. Old-style figures (also called text figures or lowercase numerals) behave like lowercase letters: 0, 1 and 2 sit within the x-height, 6 and 8 rise like ascenders, and 3, 4, 5, 7 and 9 drop below the baseline. Old-style figures blend into a paragraph the way lowercase does; lining figures match capitals and tables. Georgia, drawn for the screen in 1996, uses old-style figures by default, which is easy to see beside Times New Roman:

  • Georgia: Numbers went down 87%, where the 8 rises above the x-height and the 7 hangs below the baseline Georgia: old-style figures
  • Times New Roman: Numbers went down 87%, with both digits at capital height on the baseline Times New Roman: lining figures
The same "87%" in two faces. Georgia's figures move like lowercase; Times New Roman's stand like capitals.

The Georgia that ships with macOS has no feature for lining figures, so font-variant-numeric: lining-nums changes nothing in it, a check made in Chrome.

Proportional and tabular figures

The lecture slide "Numbers and Type" makes the second distinction. Proportional figures are designed for text: each digit is as wide as its shape needs, so a 1 is narrow and an 8 is wide. Tabular figures are designed for tables: every digit has the same width, like a monospaced font, so columns of numbers line up. The slide's point that is easy to miss: tabular figures are not limited to monospaced fonts, and many proportional text fonts include them as a feature.

Proportional and tabular figures in Inter, drawn from the font's outlinesTwo panels, each showing the numbers 111.11 and 888.88 right-aligned, with a box around each glyph's advance width. Left, Inter's default proportional figures: each 1 is 833 font units wide and each 8 is 1,267, so 888.88 is much longer than 111.11 and the two decimal points do not line up; a dashed line through each point shows the gap between them. Right, the same numbers with tabular figures: every digit is 1,328 units wide, both numbers are the same length, and the decimal points fall on one dashed line.Proportional (Inter's default)Tabular (tabular-nums)1 is 833 units wide, 8 is 1,267: the points drift apart.Every digit is 1,328 units: the points align.Units of 1/2048 em, from Inter's hmtx table.
Proportional digits vary in width, so right-aligned numbers drift; tabular digits share one width, so the decimal points line up.

Width matters twice. In a column, tabular figures line up place values, so the eye compares magnitudes by length. In a value that changes, such as a timer or a price ticker, proportional figures make the number shiver as narrow and wide digits replace each other; tabular figures keep it still.

/* Running text: the font's default, or old-style if the font has them */ article { font-variant-numeric: oldstyle-nums proportional-nums; } /* Tables, dashboards, timers: lining digits of equal width */ table, .metric, time[data-live] { font-variant-numeric: lining-nums tabular-nums; }

Tables and dashboards

Choosing and pairing took the lecture's advice for charts and dashboards from Tiffany France's article in Nightingale: a large x-height, open counters, and tabular figures, with old-style figures avoided because their rising and falling digits look ragged on an axis. In CSS, that is one declaration on the table, plus alignment:

table { font-variant-numeric: lining-nums tabular-nums; } td[data-type="number"], th[data-type="number"] { text-align: end; }

Numbers align on the end of the cell because place values start from the right: ones under ones, thousands under thousands. Align a numeric column's header with its numbers, too. With tabular figures, end alignment also lines up decimal points, as long as every value has the same number of decimal places; format them so, with Intl.NumberFormat and minimumFractionDigits in JavaScript or on the server.

True decimal alignment, where 3.5 and 108.00 align on the point whatever their digits, has been in the CSS Text Level 4 draft as text-align: ".", but no browser supports it; Chrome rejects the value in CSS.supports(). Equal decimal places are the practical answer. A font without tabular figures, like Recursive's file here, drifts whatever the CSS says.

Alignment does not reach a screen reader, which reads cell by cell; header cells with scope, units in the headers and a caption do, as tables explains.

Fractions, ordinals and the slashed zero

The rest of font-variant-numeric asks for special-purpose glyphs:

The values of font-variant-numeric
ValueFeatureWhat it does
lining-nums / oldstyle-numslnum / onumCapital-height or lowercase-style figures
proportional-nums / tabular-numspnum / tnumFigures of natural or equal widths
diagonal-fractionsfracTurns 1/2 into a fraction with a slanted bar
stacked-fractionsafrcA horizontal bar with the numbers above and below; few fonts have it
slashed-zerozeroA zero with a slash or dot, distinct from the letter O
ordinalordnRaised letters in ordinals such as 1st, or the º of Spanish 1.º

Values from different pairs combine, as in lining-nums tabular-nums slashed-zero. Three cautions come from testing them:

  • Apply fractions to the fraction. In Inter, frac is contextual and touches only digits around a slash, but in Literata it turns every digit into a numerator or denominator, so a price of 3.50 inside the same element becomes a jumble of small digits. Wrap fractions in a span, or use the precomposed ½, ¼ and ¾.
  • The slashed zero is a legibility tool for codes, serial numbers and passwords, where O and 0 must not be confused, a problem Why type matters showed across seven faces. Here only the feature-complete Inter and Literata have zero; a monospaced face that distinguishes the two by design needs no feature at all.
  • Ordinals belong to a language. English writes 1st and 2nd, French 1er and 2e, Spanish 1.º and 1.ª, and German simply "1." with a period. The ordn feature only raises letters; generate the right suffix with Intl.PluralRules and { type: "ordinal" }.

3. Small caps

Small capitals are capital letterforms drawn at about the height of the lowercase. They let a run of capitals, such as an abbreviation, sit in a line of text without shouting. Real small caps are drawn by the type designer: not just smaller, but with heavier strokes and wider spacing, so they match the weight of the text.

font-variant-caps asks for them:

The values of font-variant-caps
ValueFeatureEffect
small-capssmcpLowercase letters become small caps; capitals stay full size
all-small-capssmcp, c2scBoth lowercase and capitals become small caps: the one for abbreviations
petite-caps, all-petite-capspcap, c2pcStill smaller capitals, about the x-height in fonts where small caps are taller; falls back to small caps
unicaseunicA mix of capital and lowercase forms at one height
titling-capstitlLighter capitals designed for large sizes
abbr { font-variant-caps: all-small-caps; letter-spacing: 0.05em; }

Real and faked small caps

When the font has no smcp, browsers synthesize small caps by drawing full capitals at a smaller size. In Chrome, asking Inter for small-caps does exactly that, and petite-caps falls back to the same faked small caps, as the specification says it should. Faked small caps are thinner and lighter than the letters around them, because shrinking a capital shrinks its strokes; they look like a mistake in body text. Two properties control this:

  • font-synthesis: none turns off all faking: bold, italic, small caps and position. font-synthesis-small-caps: none turns off only the small caps. With synthesis off, a font without smcp simply shows the original letters, and the design degrades to ordinary text instead of thin imitation capitals. Both are Baseline features, and the longhand reached Safari in version 16.4 in 2023.

Small caps have a quiet advantage over capitals typed in uppercase or produced by text-transform. The characters stay what the author wrote: a screen reader, a search or a copy of "Html" set in small caps gets "Html", and an abbreviation typed as "HTML" stays "HTML". Since screen readers work from the characters, the source decides what is heard and CSS only what is seen. The abbr element marks them.

4. Ligatures and contextual alternates

A ligature joins two or three characters into one glyph, as Anatomy and vocabulary showed with Literata's "fi", where the hook of the f would otherwise hit the dot of the i. font-variant-ligatures controls four groups:

  • Common ligatures (liga, clig): fi, fl, ff, ffi. On by default. no-common-ligatures turns them off.
  • Discretionary ligatures (dlig): decorative joins such as ct or st. Off by default, for display text only.
  • Historical ligatures (hlig): forms like the long s, for period flavor. Off by default.
  • Contextual alternates (calt): glyphs chosen by their neighbors, as in a script font whose letters join differently by position. On by default. no-contextual turns them off.

The keyword none turns off every group, and normal restores the defaults. Ligatures change only glyphs, not characters, so copying "office" from a page with an "ffi" ligature still gives six letters. Letter-spacing also suppresses them: CSS Text says browsers should not apply optional ligatures when spacing between letters is not zero, so a tracked heading in Literata loses its "fi" by itself.

Contextual alternates are more powerful than their name suggests. In this section's Inter, calt turns -> into an arrow, => into a double arrow, 1x2 into a multiplication sign between the digits, while leaving 0x2F alone. Pleasant in prose; not in code.

Ligatures in code

Code is text where every character matters and where the reader often needs to see exactly what was typed. A ligature that joins "fi" in a variable name hides nothing important, but a contextual alternate that draws -> as an arrow hides two characters behind one glyph, and a reader cannot tell whether the source holds the arrow character U+2192 or a hyphen and a greater-than sign. For code in a text face, turn the substitutions off:

code, kbd, samp, pre { font-family: ui-monospace, "Cascadia Code", Menlo, Consolas, monospace; font-variant-ligatures: none; }

Some programmers want the opposite. Programming ligature fonts such as Fira Code, JetBrains Mono and Cascadia Code draw != as ≠, >= as ≥ and => as an arrow, on purpose, as one glyph spanning two or three character cells so the code's alignment survives. Supporters find operators easier to read; the typographer Matthew Butterick's essay "Ligatures in programming fonts: hell no" makes the case against: the ligatures look like real Unicode characters, so != and ≠ become indistinguishable, and a substitution cannot know the language, so it will sometimes join characters that do not form an operator. A fair compromise: ligatures belong in a programmer's own editor, while code on a web page, which others copy and learn from, shows the characters it contains.

5. Case forms, stylistic sets and position

Case-sensitive forms

Hyphens, parentheses and the @ sign are positioned for lowercase, so in a line of capitals they sit too low. The case feature raises them to suit capitals. No font-variant value asks for it, so it is one of the few features that needs font-feature-settings: "case", applied only to text set in capitals. The feature-complete Inter and Literata have case; the Fontsource builds do not.

Stylistic sets and character variants

Many fonts offer alternative designs for some letters: a single-story a, a g with an open tail, an l with a tail so it cannot be mistaken for a capital. A stylistic set (ss01 to ss20) switches a group of them together, and a character variant (cv01 to cv99) switches one character. The numbers mean nothing outside one font, which is why the CSS route has two steps: @font-feature-values gives the font's numbers names for one family, and font-variant-alternates uses the names.

@font-feature-values "Inter" { @styleset { open-digits: 1; disambiguation: 2; } @character-variant { tailed-l: 5; } } .serial-number { font-family: "Inter", sans-serif; font-variant-alternates: styleset(disambiguation); }

The numbers are Inter's own, from the names stored in the font: ss01 is "Open digits", ss02 "Disambiguation" and cv05 "Lower-case l with tail"; other fonts number differently, so read each font's list. The pair is supported in every major browser and has been Baseline since March 2023, when Chrome 111 shipped it; Firefox and Safari had it for years before. It is verbose, but the names keep meaning in the style sheet instead of magic numbers, and apply only to their family, so a fallback font never gets another font's set 2.

Superscripts and subscripts

font-variant-position: super and sub ask for the font's own superscript and subscript glyphs (sups, subs), which are drawn at the right weight and sit on the line without changing its height. HTML's <sup> and <sub> shrink and shift ordinary glyphs, which looks lighter and can open a gap between lines. Support, though, favors the elements. When the font has no superscript glyphs, Firefox synthesizes them, while Chrome and Safari do nothing at all: in Chrome, asking the Fontsource Inter for super leaves the 2 of "mc2" full size on the baseline, which turns a formula into a different number.

So use the elements for meaning: an exponent, a chemical formula, a footnote marker. They carry semantics into the document and work in any font. font-variant-position is a refinement for fonts known to have sups and subs, set on the elements and not instead of them.

font-feature-settings: the last resort

Below the named properties sits a low-level one that takes raw feature tags:

table { font-feature-settings: "tnum" 1; } table code { font-feature-settings: "liga" 0; /* tabular figures are now off here */ }

The sample is a mistake, and it shows why the CSS Fonts specification tells authors to prefer font-variant and to keep font-feature-settings for features with no other way in. It is one property holding one list, and a later declaration replaces the list rather than adding to it, so the code in the table loses its tabular figures, as a check in Chrome confirmed. The font-variant-* properties are separate, so font-variant-numeric: tabular-nums on the table and font-variant-ligatures: none on the code compose as expected.

Use font-feature-settings for features that have no named value, such as case, and put it where nothing else needs to change. When a feature should apply wherever a font is used, the same descriptor can go inside the font's @font-face rule, which keeps it with the font instead of every selector; that descriptor reached Chrome only in version 140, in 2025, so it is Baseline only since then.

Try the features

The lab sets four samples in one of this section's fonts, twice: as the font draws them by default, and with the features chosen in the form. Every option names its feature tag and says whether the chosen file has it.

6. Typographer's punctuation

The typewriter, and after it the ASCII character set, saved keys by merging marks that typography keeps apart. One straight " stood for opening and closing quotes, one ' for single quotes, the apostrophe and the prime, and one hyphen for every dash. Unicode has all the separate marks, every font in this section except the script samples has them, and using them is most of what makes text look professionally set.

Quotes and apostrophes

Typographer's quotes, often called curly or smart quotes, come in pairs with different shapes: “double” and ‘single’ in English. The apostrophe is the same character as the closing single quote, U+2019, which is the form the Unicode Standard prefers. Straight quotes in prose read as unfinished.

A common slip is a backward apostrophe at the start of a word, as in ‘90s, made by software that assumes every quote after a space opens a quotation. It should be ’90s: the mark stands for missing digits.

Primes, multiplication and minus

Several marks look like punctuation but are something else:

  • Primes mark feet and inches (6′ 2″) and minutes and seconds of arc (32° 42′ 51″). They are U+2032 and U+2033, straight but slanted, not quotes.
  • The multiplication sign × (U+00D7) belongs in dimensions and arithmetic: 1920 × 1080, 3 × 4. The letter x is a letter, to a search engine and to a screen reader.
  • The minus sign − (U+2212) is longer than a hyphen and, in a well-made font, as wide as the plus sign and at its height, so −5 and +5 line up in a table.

Hyphens and dashes

English uses three horizontal marks, plus the minus, and the differences are length and meaning:

The hyphen, en dash, em dash and minus sign in Literata, with their widths and usesFour rows, each showing a glyph drawn from Literata's outlines inside a box of its advance width, with the baseline and the x-height as dashed lines. Hyphen, U+002D, 0.354 em wide, a short bar in the middle of the x-height, used in compound words such as well-known. En dash, U+2013, 0.5 em, a longer bar at the same height, used for ranges such as 9 to 5 and pages 10 to 12, and spaced as a dash in British style. Em dash, U+2014, a full em, used for a break in a sentence. Minus sign, U+2212, 0.582 em, the same width as the plus and multiplication signs and set slightly higher, at the height of the plus sign's bar, used for negative numbers and subtraction.HyphenU+002D, 0.354 emcompound words: well-knownand line-end hyphenationEn dashU+2013, 0.5 emranges: 9–5, pages 10–12;a spaced dash in British styleEm dashU+2014, 1 ema break in a sentence—like this,closed up in US styleMinus signU+2212, 0.582 em−5 °C, 7 − 3: as wide as + and ×,at the height of the plus sign's bardashed lines: baseline and x-height
Three dashes and a minus, measured in Literata. The en dash is half an em and the em dash a full em; the minus matches the plus sign, not the hyphen.

Style guides differ on the em dash. American usage closes it up—like this—while British usage, and German and French, prefer a spaced en dash – like this. Either is right if consistent; a hyphen or two standing in for a dash is not.

Ellipses and spaces

The ellipsis … (U+2026) is one character with spacing chosen by the font's designer; three periods typed in a row are spaced as three separate periods. Some editors prefer spaced periods; it is a house-style decision.

Spaces come in several widths and with different line-breaking rules, and the rules matter more than the widths:

Spaces worth knowing
SpaceCode pointBreaks a line?Used for
No-break spaceU+00A0, &nbsp;NoKeeping a number with its unit (10 km), a title with a name (Dr. Rivera), and before the French colon
Narrow no-break spaceU+202F, no named entityNoFrench spacing inside guillemets and before ; ! ?; thousands separators in French and in SI style (10 000)
Thin spaceU+2009, &thinsp;YesA little air, for example around a spaced dash; it can break, so it does not hold anything together

The narrow no-break space is newer than the others, and fonts lag: of this section's files, only the feature-complete Inter has a glyph for U+202F, so elsewhere the browser takes it from a fallback font. For a space that is usually harmless, but check a font's character set as well as its features. The no-break space has a cost of its own: a long run of them makes text that cannot reflow, so use one where two words must stay together, not to push things into place.

Typing and encoding the marks

With UTF-8, which every page should declare, any of these characters can simply be typed into the HTML. HTML syntax explains why UTF-8 made most character references unnecessary, and character references covers the cases where they are still needed: when a character is invisible in the source, like the no-break spaces, or when an editor or a pipeline cannot be trusted with it.

<p>It’s 9–5, “mostly” — 6′ 2″ × 3 … −5 °C</p> <p>It&rsquo;s 9&ndash;5, &ldquo;mostly&rdquo; &mdash; 6&prime; 2&Prime; &times; 3 &hellip; &minus;5&nbsp;&deg;C</p> <!-- No named reference exists for the narrow no-break space --> <p lang="fr">&laquo;&#x202F;Bonjour&#x202F;&raquo;</p>

On the keyboard, macOS types the curly quotes with Option and the bracket keys, the en dash with Option-hyphen and the em dash with Shift-Option-hyphen. Windows has Alt codes on the numeric keypad (Alt+0150 for the en dash, Alt+0151 for the em dash, Alt+0147 and Alt+0148 for the double quotes), and Linux desktops offer a Compose key. In practice, most curly quotes on the web come from software that converts as you type or at build time.

Quotation marks in other languages

Quotation marks are not universal. German opens low and closes high, „so“ with ‚single‘ quotes inside, and German books also use »reversed guillemets«. French uses « guillemets » with narrow spaces inside. Japanese uses corner brackets, 「like this」. Type for people, page 10, takes these languages further.

HTML can choose the marks for you. The q element, for a short inline quotation (text-level semantics), gets its marks from CSS through the quotes property, which defaults to auto: marks appropriate to the element's language. In Chrome, a nested q inside <p lang="de"> renders „…‚…‘…“, in lang="ja" 「…」, and in lang="fr" «…» but without the narrow spaces French typography expects. CSS for people shows :lang(), which can set quotes explicitly where the defaults fall short:

:root { quotes: auto; } :lang(fr) { quotes: "«\202F" "\202F»" "“" "”"; }

This keeps the marks out of the text, so the lang attribute, not the author's keyboard, decides them.

Never in code

Every one of these conversions is wrong inside code. A curly quote is not a string delimiter, an en dash is not a minus or an option flag, and a no-break space is not whitespace to most programming languages. Commands copied from a blog that "smartened" them fail in confusing ways. Any converter, whether a CMS plugin, a Markdown processor's typography option or a hand-written build step, must skip code, pre, kbd and samp, and the attribute values in the HTML around them.

Hanging punctuation

A paragraph that opens with a quotation mark looks indented by the mark's width, because the mark sits inside the text block. Typographers hang it in the margin, so the letters align with the lines below. The CSS property is hanging-punctuation, with values first, last, allow-end and force-end:

blockquote p { hanging-punctuation: first allow-end; }

As of October 2026 only Safari supports it, and only in Safari 26.5 does first also hang straight quotes; Chrome and Firefox ignore the declaration. It is a pure enhancement: where it is not supported, the quotation simply sits inside the block, as it always has, so it is safe to use.

7. Dingbats and symbol fonts

Printers always had ornaments cast in metal like letters. Digital dingbat fonts, whose glyphs are pictures, continue the tradition, and two of them show the right and the wrong way to do it.

  • Zapf Dingbats: scissors, a telephone, an airplane, an envelope, a victory hand, pencils and pens, check marks, crosses and ballot X marks Zapf Dingbats, from the characters U+2701 to U+271D
  • Wingdings: hand gestures, faces, a bomb, a skull and crossbones, flags, an airplane, sun, droplet, snowflake, crosses, a star of David and a crescent Wingdings, from the letters A to Z
Two symbol fonts, drawn by macOS. The first row comes from Unicode symbols; the second from the ordinary capital letters A to Z.

ITC Zapf Dingbats, drawn by Hermann Zapf and released in 1978, became one of the 35 fonts built into PostScript printers with Apple's LaserWriter Plus, so it was everywhere desktop publishing went. When Unicode was assembled, its symbols were given code points of their own, most of them in the Dingbats block that starts at U+2700: the scissors are U+2702, the check mark U+2713. The first specimen was produced by typing those characters. Text that contains them says "check mark" to any software that reads it, and any font with the character can draw it.

Wingdings, which Microsoft made in 1990 from symbol fonts licensed from Charles Bigelow and Kris Holmes, took the other route. Its pictures sit on the code points of ordinary letters: the second specimen was produced by typing A to Z. The text is really letters, and the font only paints over them. That breaks as soon as the text leaves the font:

  • Copy and paste, screen readers and fallback all get the letters: a row of "check marks" is read, and shown without the font, as a string of capitals.
  • Meaning appears that nobody intended. In 1992, shortly after Windows 3.1 shipped, people noticed that typing "NYC" in Wingdings gives a skull and crossbones, a Star of David and a thumbs up, and read it as an antisemitic message. Microsoft called the arrangement coincidence; when it made the Webdings font in 1997, the same letters were chosen to give an eye, a heart and a city.

Many Wingdings pictures later received their own Unicode code points, in Unicode 7.0. The rule that follows applies beyond dingbats: the characters in a document should mean what they show. A font changes how a character looks, never what it is.

8. Icon fonts, and why SVG replaced them

Around 2012, the Wingdings idea came back for web icons. An icon font, such as early Font Awesome or GitHub's Octicons, packs a site's icons into one web font, usually on code points in Unicode's Private Use Area, a range set aside for characters whose meaning only the font knows. CSS then inserts them:

<style> .icon-search::before { font-family: "SiteIcons"; content: "\e900"; /* a Private Use Area code point */ } </style> <button><span class="icon-search"></span></button>

It was attractive: one request, and icons that take the text color and scale with the text. It also inherited every weakness of a font, plus those of Wingdings:

  • Accessibility. The icon is a character with no meaning outside the font. A screen reader may announce a Private Use Area character as nothing, as an unknown symbol or as a code point, and a button containing only an icon has no accessible name at all. Authors had to add aria-hidden="true" to the icon and hidden text to the control, and often did not.
  • Font loading failures. If the font is blocked, slow or fails, the reader sees empty boxes or nothing, for the very controls they need.
  • Font overrides. A reader who replaces web fonts with one they read more easily, such as a dyslexia-friendly face, replaces the icons too, and gets boxes.
  • Rendering. Icons get anti-aliasing and hinting meant for letters, which can blur them, and one glyph is one color.

GitHub's post "Delivering Octicons with SVG" (February 2016) gave these reasons for switching its whole site: icons that were blurry in some browsers, a flash while the font loaded, and readers who override fonts seeing empty squares. SVG icons are images, not text, so none of the font problems apply: an inline svg can have a title or be marked decorative, uses currentColor to take the text color, renders crisply, and can have several colors. SVG icon systems shows the symbol and use pattern, and accessible SVG how to name or hide an icon:

<button type="button"> <svg aria-hidden="true" width="16" height="16"><use href="/icons.svg#search"/></svg> <span class="visually-hidden">Search</span> </button>

Icon fonts survive in older projects; for new work, SVG is the default.

9. Emoji

Emoji look like pictures, but they are characters, in the same text stream as letters, with Unicode code points, names and properties. A font makes them pictures; being text makes them meaningful to software. That puts them on the right side of the rule from section 7.

Emoji fonts

Ordinary glyphs are outlines filled with one color. Emoji need color, and the industry produced four formats inside OpenType, which is why the same emoji looks different on each platform:

  • COLR/CPAL: glyphs built from layers of ordinary outlines, each with a color from a palette. Version 0 has flat colors, works in every major browser and is used by Microsoft's Segoe UI Emoji. Version 1 adds gradients; Chrome has supported it since version 98 and Firefox since 107, Safari not yet.
  • sbix and CBDT/CBLC: bitmap images per glyph, from Apple (Apple Color Emoji) and Google (the older Noto Color Emoji).
  • SVG in OpenType: an SVG document per glyph, from Adobe and Mozilla, supported by Firefox and Safari but not by Chrome.

For web developers, the consequence is that emoji come from the color emoji font on the reader's system, so the page cannot know exactly what the reader will see; font-variant-emoji, below, can only ask for a form, and Fonts in CSS explains why the emoji generic family was dropped from the specification.

Text or emoji presentation

Some characters existed long before emoji, such as the heart U+2764, the check mark U+2714 and the smiling face U+263A, and they can be drawn either as a black-and-white text symbol or as a color emoji. Two invisible variation selectors choose: U+FE0E after the character asks for text presentation, U+FE0F for emoji presentation. Without one, the platform decides.

CSS adds font-variant-emoji, with values normal, text, emoji and unicode (follow Unicode's default for each character). Chrome supports it from version 131 and Firefox from 141; Safari has it only in its Technology Preview, so as of October 2026 it is not Baseline. It is a request, not a command. In Chrome, emoji turned the plain smiling face into color and text turned it back, but a grinning face stayed in color: no font had a text version. For a symbol that must look like text, such as a check mark in a table, the variation selector in the content is the more dependable choice.

Modifiers and sequences

Many emoji are several characters drawn as one glyph. Five skin tone modifiers (U+1F3FB to U+1F3FF), added in Unicode 8.0 in 2015, follow a person or a hand. The zero-width joiner (U+200D) glues emoji into a sequence that a font may draw as one picture:

An emoji sequence: four code points drawn as one glyphTop: four boxes for the code points U+1F469 woman, U+1F3FD medium skin tone, U+200D zero-width joiner, and U+1F4BB personal computer. An arrow leads to a single glyph, woman technologist: medium skin tone, the name a screen reader announces. Beneath, a note: in JavaScript the string has a length of 7, because each emoji outside the Basic Multilingual Plane takes two UTF-16 code units, but it is one grapheme, one user-perceived character. A font without the sequence draws the parts side by side. Bottom: two smaller rows show variation selectors. U+2764 heart followed by U+FE0F gives emoji presentation, a color heart. U+2764 followed by U+FE0E asks for text presentation, a black heart, if a font has one.U+1F469womanU+1F3FDmedium skin toneU+200Dzero-width joinerU+1F4BBpersonal computerone glyphwoman technologist:medium skin tone4 code points, a JavaScript length of 7, one grapheme. A font without the sequence draws the parts side by side.U+2764 heartU+FE0Femoji presentation: color heartU+2764 heartU+FE0Etext presentation, if a font has itinvisible characters(shaded) choose the form
Several code points, one picture: a skin tone modifier and a zero-width joiner build a sequence, and variation selectors choose between text and emoji forms.

The sequence is one grapheme, one character as a reader perceives it, but four code points and seven UTF-16 code units, so "…".length in JavaScript says 7. Code that truncates text, counts characters for a limit or reverses a string must use Intl.Segmenter with granularity: "grapheme", or it will cut emoji in half and leave broken pictures, a point Anatomy and vocabulary made about characters and glyphs in general.

Emoji and accessibility

Because emoji are characters, screen readers read them, by their Unicode names: a party popper is read as "party popper", and five clapping hands are read as "clapping hands" five times. An emoji used as punctuation in every heading, or a row of them as decoration, becomes a recitation. Names describe the picture, not the intent. Three habits help:

  • Use emoji in moderation and never as the only label of a control or the only sign of a status.
  • When an emoji is decoration, hide it from assistive technology: <span aria-hidden="true"> around it.
  • When it carries meaning that its name does not, give it one: <span role="img" aria-label="Launch day">.

Emoji also cross cultures less smoothly than they seem: a gesture friendly in one place is rude in another, and one platform's smile is another's grimace.

10. Summary

The details, in brief
ConceptKey points
FeaturesAlternates live in the font as OpenType features with four-letter tags; CSS asks, the font answers. Subsets can drop features; check the file.
FiguresLining or old-style; proportional or tabular. lining-nums tabular-nums for tables, dashboards and changing values.
TablesAlign numbers to the end with equal decimal places; text-align: "." is unsupported.
Fractions, zero, ordinalsApply diagonal-fractions to the fraction only; slashed-zero for codes; ordinals differ by language, so generate them with Intl.PluralRules.
Small capsall-small-caps for abbreviations. Without smcp the browser fakes thin capitals unless font-synthesis-small-caps: none.
LigaturesCommon and contextual on by default; turn them off in code. Programming ligatures are a personal editor setting.
Alternates and positioncase needs font-feature-settings; stylistic sets use @font-feature-values and font-variant-alternates (Baseline 2023). font-variant-position does nothing in Chrome and Safari without the font's glyphs; keep sup and sub.
font-feature-settingsOne list that each declaration replaces. Prefer font-variant-*; raw tags only for features with no named value.
PunctuationCurly quotes and the apostrophe ’; primes; × and −; en dash for ranges, em dash for breaks; ellipsis; no-break spaces. Quotes vary by language, and q with lang chooses them. Never convert code. hanging-punctuation is Safari only.
DingbatsZapf Dingbats became Unicode characters (from U+2700), so the text means what it shows; Wingdings paints over letters, which copy, screen readers and fallback reveal.
Icon fontsPrivate Use Area characters that screen readers cannot name and that turn to boxes when the font fails or is overridden. SVG replaced them.
EmojiCharacters drawn by system color fonts. Variation selectors and font-variant-emoji (Chrome, Firefox) choose the form. Sequences are one grapheme of many code points. Screen readers read their names.