CSS Typography and Character Encoding Explained
Learn why character encoding and font selection solve different problems, how to use UTF-8, and how to diagnose broken or missing characters.
Character encoding and CSS typography solve different problems. Encoding turns document bytes into characters. Fonts and CSS determine how those characters are shaped and displayed. If text appears as mojibake, changing the font will not fix it. If the text is decoded correctly but a character appears as a box or an unsuitable glyph, check font coverage and shaping support.
For most web pages, use UTF-8 consistently, declare it in HTML, save CSS as UTF-8, and choose a font stack with a generic fallback. Then test the real languages and symbols your page uses.
1. Encoding and typography are separate layers
A browser receives bytes. It must interpret those bytes using a character encoding before it can work with the document’s characters. UTF-8 is the recommended choice for HTML and CSS. After decoding, CSS font matching maps characters to glyphs in available font faces and applies typographic properties such as size, weight, spacing, and OpenType features.
A useful diagnostic distinction:
- Wrong characters, such as garbled accented text: investigate the file bytes and encoding declarations first.
- Correct text with a missing-glyph box, odd symbol, or a different-looking character: investigate font coverage, fallback, and script shaping.
The W3C CSS Fonts Level 4 Working Draft describes font matching as operating on runs of Unicode characters. A font change cannot undo incorrect decoding that happened earlier.
2. Declare and preserve UTF-8
Put the character-set declaration near the start of the HTML document, ideally directly after the opening head tag:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Typography example</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<p>Résumé — café — Ελληνικά — 日本語</p>
</body>
</html>
The HTML Standard requires the encoding declaration to be entirely within the first 1,024 bytes when using this declaration route. New authoring tools are required to default to UTF-8. A server may also provide encoding metadata; make sure that does not conflict with the document.
Save the HTML source itself as UTF-8. Save CSS as UTF-8 as well. When the HTML and stylesheet share the same encoding, a separate CSS declaration is generally unnecessary. If a stylesheet truly uses another encoding, CSS @charset has strict placement and byte-sequence rules; typing an at-rule into a file saved in the wrong encoding does not convert the file.
3. Build a font stack with fallback
A font-family value is a prioritized list. The browser tries the preferred family and can fall back when that face is unavailable or lacks a glyph. Include a generic family at the end:
body {
font-family: "Preferred Web Font", "Local UI Font", sans-serif;
font-size: 1rem;
line-height: 1.5;
}
Fallback can occur character by character. Most of a sentence may use the preferred font while one symbol or character from another script comes from a fallback face. That character may therefore look different even though the page’s main font appears to load correctly. Generic families such as serif, sans-serif, and monospace let the browser choose an appropriate installed face if the named choices cannot supply the text.
4. Understand the typography controls
Typography is more than family selection. These CSS properties independently affect appearance and layout:
| Control | Common CSS | What to check |
|---|---|---|
| Family | font-family |
Priority order, actual availability, script coverage, and fallback. |
| Size | font-size |
Legibility at the intended viewport and zoom. |
| Weight | font-weight |
Whether the requested weight exists or must be synthesized. |
| Width and style | font-stretch, font-style |
Whether the face supports condensed, expanded, italic, or oblique variants. |
| Spacing | line-height, letter-spacing, word-spacing |
Readable line rhythm and any script-specific spacing problems. |
| Features | font-variant, font-feature-settings |
Optional ligatures, alternate numerals, fractions, and other OpenType behavior. |
Variable fonts can provide multiple supported variations, such as a range of weights, in one font resource. Check the font’s available axes and declared ranges rather than assuming every value is supported.
5. Decide whether to use a web font
A system stack is simple and avoids downloading a font resource, but appearance varies with the user’s available fonts. A downloadable web font gives more control over the intended design, while adding a network resource and a licensing decision. Static font faces use separate resources for styles; a variable font may cover multiple variations in one resource.
With a licensed font file, a basic @font-face setup looks like this:
@font-face {
font-family: "Site Sans";
src: url("/fonts/site-sans.woff2") format("woff2");
font-style: normal;
font-weight: 400;
font-display: swap;
}
body {
font-family: "Site Sans", Arial, sans-serif;
}
Use the formats, weight ranges, language subsets, and embedding instructions supplied by the font provider. Licensing depends on the provider and intended use. Hosted services may require their embed code; self-hosting may require an appropriate license. Do not assume a desktop font license covers web embedding.
6. Diagnose broken or missing characters
- Identify the symptom. Mojibake points toward decoding; a missing glyph or inconsistent character shape points toward font selection or shaping.
- Inspect the original source. Confirm the file is actually saved as UTF-8. An editor’s displayed text alone does not prove the on-disk encoding.
- Check declarations and response metadata. Verify the HTML meta declaration and any server-provided charset agree with the actual bytes.
- Check the stylesheet bytes. Confirm CSS is saved as UTF-8. Avoid adding
@charsetas a guess; it does not transcode a file. - Check the chosen font and fallback. Test the missing character in the preferred face and the generic fallback. Confirm the resource loaded and the face contains the needed glyph.
- Test representative content. Include the actual scripts, accented letters, symbols, combining marks, and punctuation used by the site. A Latin-only sample cannot establish multilingual support.
- For complex scripts, check shaping. Glyph coverage alone may not be enough; shaping tables, mark positioning, combining sequences, and variation sequences can affect the rendered result.
7. Common problems and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Accents display as unrelated symbols or sequences. | Bytes decoded using the wrong encoding, or an encoding declaration conflicts with the file. | Save the source as UTF-8 and align the HTML declaration and response metadata. |
| A replacement box appears for one symbol. | No selected or fallback font has its glyph. | Use a font with that character and verify the fallback stack. |
| One character looks unlike neighboring text. | Per-character fallback selected another font. | Choose a family with broader coverage or accept and test the fallback appearance. |
| A script’s letters appear disconnected or marks are misplaced. | Insufficient shaping support, unsuitable font, or incomplete font resource. | Test the actual script with a font that supports its shaping behavior and required marks. |
| Custom font never appears. | Resource URL, format, loading, or font-face descriptors are wrong. | Check the network request and console, then verify the URL, format, style, weight, and license-based delivery setup. |
| CSS characters are wrong despite correct HTML. | The stylesheet has different bytes or encoding from what is assumed. | Save CSS as UTF-8 and keep it aligned with the document; only use @charset when genuinely required. |
8. Performance, reliability, and cost
System fonts avoid a font download, while web fonts require delivery and may affect when the intended face becomes visible. Keep the font resources and language subsets appropriate to the content, and declare only the faces and ranges needed. font-display: swap can show fallback text while a web font loads, but the fallback may differ in metrics and cause layout changes. Test loading behavior and text layout on the networks and devices you support.
For reliability, preserve a sensible fallback stack so text remains readable if a custom resource fails. Test offline or blocked-resource behavior where it matters, and check real multilingual content in the browsers and operating systems relevant to your audience. Font file costs and licensing terms depend on the provider and deployment arrangement; confirm those terms before publishing.
9. Inspect the rendered result
When a page looks correct locally but differs in a deployed environment, inspect the rendered page at the target viewport and compare the actual text, not just a Latin sample. A screenshot is useful for visual review of line breaks, fallback glyphs, and spacing across viewports. ScreenshotNeo is a website screenshot API and MCP server for developers; its capture options include viewport and device presets, full-page capture, and custom CSS or JavaScript. See the ScreenshotNeo API documentation for request options.
10. Or skip the browser setup
To capture a page with ScreenshotNeo, send one request with the target URL. The example saves the response as WebP; use the API documentation for output and other capture options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture, and each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers identify the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for AI agents. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Sign up for 1,000 free screenshots a month, with no card required.
11. FAQ
Can CSS fix text that was decoded incorrectly?
No. Correct the source bytes and encoding interpretation first; CSS controls appearance after decoding.
Does a UTF-8 declaration guarantee every character will look right?
No. The selected fonts still need suitable glyph coverage and, for some scripts, shaping support.
Why does only one character use a different-looking font?
Font fallback can select a different face for an individual character that the preferred font does not support.
Do I need a custom web font?
No. A system stack can be sufficient. Use a custom font when its appearance or coverage is needed and its delivery and license terms work for the project.


