CSS text-transform and Spacing
CSS gives you three properties that reshape how text looks without touching the underlying HTML content: text-transform changes letter case for display purposes, while letter-spacing and word-spacing control the gaps between characters and words. Together they’re essential for typography — turning labels into small caps, loosening tight headline type, or fixing cramped body copy — and they show up constantly in real design systems (badges, navigation menus, buttons, all-caps eyebrow text).
Overview / How it works
text-transform is purely a rendering instruction. It does not alter the actual characters stored in the DOM or in the accessibility tree in most respects for casing — the text node still contains the original string; the browser just paints a case-mapped version of it. This matters for two reasons: copy-pasting the text yields the original casing (not what you visually see), and CSS text-transform is Unicode-aware, meaning it follows language-specific case-folding rules, not a naive per-character swap. For example, transforming the German letter ß to uppercase in a Unicode-correct implementation can produce SS, and some locales have special casing rules for letters like the Turkish dotless i.
letter-spacing and word-spacing operate at the text-layout stage, after the browser has shaped the text run (determined glyph positions via the font’s shaping engine) but before final line box measurement. Conceptually, the rendering engine walks the shaped glyph run and inserts extra advance width after each character (for letter-spacing) or after each occurrence of a word-separator character such as a space or tab (for word-spacing). Because this extra space is added to the glyph’s advance width, it changes how much horizontal room the text occupies, which in turn affects line wrapping, text overflow, and how many words fit on a line. Both properties accept negative values, which pull characters or words closer together instead of pushing them apart — useful for tightening oversized display headlines, but risky because too negative a value causes glyphs to visually overlap.
None of these three properties are inherited-by-default exceptions — all three are inherited properties, meaning a value set on a parent element (like body or a wrapping <section>) cascades down to descendant text nodes unless overridden. This makes them convenient to set globally (for example, uppercase button text sitewide) and equally easy to accidentally leak into places you didn’t intend.
Syntax
selector {
text-transform: none | capitalize | uppercase | lowercase | full-width | full-size-kana;
letter-spacing: normal | <length>;
word-spacing: normal | <length>;
}
| Property | Common values | Description |
|---|---|---|
text-transform |
none, capitalize, uppercase, lowercase |
capitalize uppercases the first letter of each word; uppercase/lowercase transform every letter; none disables any inherited transform. |
text-transform (less common) |
full-width, full-size-kana |
Typographic adjustments for East Asian typesetting — map characters to full-width forms or convert small kana to full-size kana. |
letter-spacing |
normal, 0.05em, 2px, -1px |
Extra space inserted between each character. Must use a length unit — unitless numbers (other than 0) are invalid. |
word-spacing |
normal, 0.25em, 4px |
Extra space added at each word-separator (typically the space character). Has little effect on scripts that don’t use spaces between words, such as Chinese or Japanese. |
Examples
Example 1: Basic case transforms
.eyebrow {
text-transform: uppercase;
letter-spacing: 0.08em;
font-size: 0.8rem;
color: #6b7280;
}
.article-title {
text-transform: capitalize;
}
.legal-fineprint {
text-transform: lowercase;
}
Result: An element with class eyebrow containing the text “new release” renders as “NEW RELEASE” in small, slightly gray, letter-spaced capital text — a classic “eyebrow” label above a headline. An article-title containing “the quick brown fox” renders as “The Quick Brown Fox”, with the first letter of every word capitalized. The legal-fineprint block forces everything to lowercase regardless of how it was typed in the HTML.
This example shows the three primary keyword transforms side by side. Note that capitalize capitalizes the first letter of each whitespace-separated word — it does not know about grammar, so short words like “the” or “of” still get capitalized, which is sometimes not what a designer wants for true title-case.
Example 2: Tightening a large display headline
.hero-heading {
font-size: clamp(2.5rem, 6vw, 5rem);
font-weight: 800;
letter-spacing: -0.02em;
line-height: 1.05;
}
.hero-heading .badge {
display: inline-block;
text-transform: uppercase;
letter-spacing: 0.1em;
font-size: 0.75rem;
font-weight: 700;
padding: 4px 10px;
border: 1px solid currentColor;
border-radius: 999px;
}
Result: The large heading text sits slightly more tightly packed than its default spacing (negative letter-spacing is a common technique at very large font sizes, where default spacing looks unnaturally loose). Inside it, a small pill-shaped badge shows uppercase text with generous positive letter-spacing, giving it a distinct, deliberately spaced-out look that contrasts with the tight headline.
This demonstrates the two opposite directions letter-spacing is used in practice: negative values to compact very large headline type, and positive values to open up small uppercase labels so the capital letters don’t feel cramped.
Example 3: word-spacing in justified body text
.pull-quote {
text-align: justify;
word-spacing: 0.15em;
font-style: italic;
max-width: 40ch;
}
.pull-quote::first-line {
text-transform: uppercase;
letter-spacing: 0.04em;
}
Result: A justified paragraph of quoted text gets noticeably wider gaps between words than normal body copy, giving it a more spaced, editorial feel. Its very first line of text renders in uppercase with slightly increased letter spacing, a common magazine-style effect achieved through the ::first-line pseudo-element, while the rest of the paragraph keeps normal casing.
This example combines word-spacing with a pseudo-element to show that these properties compose naturally with other text and layout features like justification and first-line styling.
How it works step by step
- Text transform application: before layout, the engine takes each text node’s string and, for painting purposes, produces a case-mapped copy according to the current
text-transformvalue and the element’s language (langattribute), using Unicode case-folding tables rather than simple ASCII shifting. - Text shaping: the (possibly transformed) string is fed to the font’s shaping engine, which selects glyphs and computes each glyph’s natural advance width based on the font’s metrics and any OpenType features (kerning, ligatures).
- Spacing insertion: the layout engine then walks the shaped run. For
letter-spacing, it adds the specified length to the advance width after each glyph (this can also suppress ligatures, since letter-spacing implies each character must remain individually positionable). Forword-spacing, it adds the specified length specifically at word-separator characters. - Line box measurement: the now-widened run is measured to decide line breaks, overflow, and (if
text-align: justifyis set) how much additional space justification needs to distribute — extra letter/word spacing reduces how much justification has to stretch things on its own. - Paint: glyphs are finally painted at their computed positions, so the visual gaps you see are the sum of the font’s natural spacing plus your explicit
letter-spacing/word-spacingadditions.
Common Mistakes
Mistake 1: Omitting the unit on letter-spacing
.tag {
letter-spacing: 2;
}
This looks reasonable but is invalid — letter-spacing requires a length (like 2px or 0.1em), and a bare number other than 0 is not a valid length. Browsers will ignore this declaration entirely, silently leaving the default spacing in place.
.tag {
letter-spacing: 2px;
}
Adding the unit fixes it. Using em units (relative to the element’s own font size) is usually preferable to a fixed pixel value, since spacing then scales proportionally if the font size changes responsively.
Mistake 2: Relying on text-transform to change stored/searchable data
.username-display {
text-transform: uppercase;
}
A developer applies this and then writes JavaScript or a search feature that assumes the text content is uppercase — but text-transform only changes what’s painted on screen, not the underlying text node. Copying the text, running textContent in a script, or comparing search input against the displayed text will still see the original mixed-case string, leading to confusing bugs (“the search didn’t match even though it looks identical on screen”).
If you actually need the stored value to be uppercase (for comparisons, storage, or display consistency across contexts), transform the string itself at the data layer, and use text-transform purely as a presentational nicety on top of that.
Mistake 3: Excessive negative letter-spacing causing overlap
.crammed-heading {
font-size: 4rem;
letter-spacing: -0.3em;
}
A value this aggressively negative will push most fonts’ glyphs into each other, producing overlapping, unreadable letters — a very common mistake when someone copies a “tight headline” value meant for a much smaller font size. Negative letter-spacing should be a small fraction of the font size, typically no more than about -0.02em to -0.05em for large display type.
.crammed-heading {
font-size: 4rem;
letter-spacing: -0.02em;
}
This keeps the tightened, modern look without breaking legibility.
Best Practices
- Use
emunits forletter-spacingandword-spacingso the gap scales with the element’s own font size instead of staying a fixed pixel amount at every size. - Reserve
text-transform: uppercasefor short labels, buttons, and eyebrows — long paragraphs in all caps are noticeably harder to read and can feel like shouting. - Never rely on
text-transformto sanitize or normalize data used for comparisons, search, or storage; it is presentation-only. - When using
uppercaseon already-capitalized acronyms or proper nouns in multiple languages, test with real content — Unicode case mapping can produce unexpected results (for example, certain accented characters or the Germanß). - Keep positive
letter-spacingmodest for lowercase body text — wide letter-spacing reads well on uppercase labels but often looks odd and reduces legibility on mixed-case paragraphs. - Test any large negative
letter-spacingvalue against the actual font and weight you’re shipping with, since different typefaces have very different natural character widths.
Practice Exercises
- Exercise 1: Style a navigation menu (
nav a) so its links appear in uppercase with letter-spacing of about0.05em, and confirm the underlying link text in your HTML can stay written in normal sentence case. - Exercise 2: Create a
.subtitleclass that usestext-transform: capitalize, then test it against a phrase containing short connector words like “a tale of two cities” — observe and explain in your own words why every word gets capitalized, including “of”. - Exercise 3: Build a justified paragraph (
text-align: justify) and experiment withword-spacingvalues from0up to0.5em, noting at what point the extra gaps between words start to look uneven or distracting.
Summary
text-transformchanges displayed letter case (none,capitalize,uppercase,lowercase) without altering the underlying text content.letter-spacingadds (or, with negative values, removes) space between individual characters; it requires a length unit.word-spacingadds space specifically at word-separator characters like spaces, and has minimal effect on scripts without spaces between words.- All three properties are inherited, are applied during text shaping and layout, and directly affect line-wrapping and justification.
- Unicode-aware case mapping means
text-transformcan behave differently across languages — don’t assume a naive one-to-one character swap. - Use these properties for presentation only; never depend on them for data comparison, search, or storage logic.
