Text Decoration and Transformation

▶ Try It Yourself

CSS provides fine-grained control over how text is decorated (underlines, strikethroughs, highlights) and transformed (uppercase, lowercase, capitalised). These properties are often underestimated — a precisely styled underline can be a significant branding element, and text-transform is the correct way to enforce visual casing without changing the underlying text. In this lesson you will master every decoration and transformation property, including the modern underline styling API.

Text Decoration Properties

Property Controls Example Values
text-decoration-line Which line(s) to draw underline, overline, line-through, none
text-decoration-color Line colour (independent of text colour) #4f46e5, currentColor, transparent
text-decoration-style Line style solid, dotted, dashed, wavy, double
text-decoration-thickness Line stroke width 2px, 0.1em, from-font
text-underline-offset Gap between text baseline and underline 3px, 0.2em — positive moves line down
text-decoration Shorthand for line, style, color, thickness underline dotted #4f46e5 2px

text-transform Values

Value Effect Best Used For
uppercase ALL CAPS regardless of HTML Labels, badges, navigation items, section headings
lowercase all lowercase Email display, code-like styling
capitalize First Letter Of Each Word Titles — prefer real title case in HTML for accessibility
none Original casing restored Reset inherited transform

text-shadow

Syntax Parameters Common Use
text-shadow: 2px 2px 4px rgba(0,0,0,0.3) x-offset y-offset blur colour Contrast on image text
text-shadow: 0 0 20px rgba(79,70,229,0.5) No offset — glow effect Neon / glow text effects
text-shadow: 1px 1px 0 #fff Hard shadow, no blur Emboss / deboss effects
Multiple shadows Comma-separated list Stack multiple effects
Note: text-transform: uppercase changes only the visual rendering — the underlying HTML text is unchanged. Screen readers read the original casing. This is the correct approach: use CSS for visual presentation, keep the HTML semantically correct. Never type text in ALL CAPS in your HTML just to make it appear uppercase visually.
Tip: Combine text-underline-offset and text-decoration-color: transparent with a hover transition for elegant link underline reveal effects — the underline is always present (preserving layout stability) but only visible on hover. This avoids the layout shift caused by adding/removing underlines that affect text flow.
Warning: Removing underlines from links with text-decoration: none harms accessibility — links must be visually distinguishable from regular text by colour alone, which fails WCAG colour contrast requirements for users with colour-blindness. Always provide a non-colour visual indicator (underline, bold, border) for links within body text.

Basic Example

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
  <title>Text Decoration and Transformation</title>
  <style>
    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
    html { font-size: 100%; }
    body { font-family: 'Inter', system-ui, sans-serif; padding: 40px 32px; background: #f8fafc; color: #1e293b; max-width: 780px; margin: 0 auto; }
    h3   { font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: #64748b; margin: 32px 0 12px; }
    .demo-row { display: flex; flex-wrap: wrap; gap: 24px; margin-bottom: 12px; align-items: center; }

    /* ── Custom link underlines ── */
    .link-default   { color: #4f46e5; }
    .link-custom {
      color: #4f46e5;
      text-decoration-line: underline;
      text-decoration-color: #a5b4fc;
      text-decoration-thickness: 2px;
      text-underline-offset: 4px;
      transition: text-decoration-color 0.2s;
    }
    .link-custom:hover { text-decoration-color: #4f46e5; }

    .link-wavy {
      color: #0f172a;
      text-decoration-line: underline;
      text-decoration-style: wavy;
      text-decoration-color: #ef4444;
      text-underline-offset: 5px;
    }

    .link-animated {
      color: #0f172a;
      text-decoration-line: underline;
      text-decoration-color: transparent;
      text-decoration-thickness: 2px;
      text-underline-offset: 4px;
      transition: text-decoration-color 0.2s ease;
    }
    .link-animated:hover { text-decoration-color: #4f46e5; }

    /* ── Strikethrough and overline ── */
    .strike { text-decoration: line-through; text-decoration-color: #ef4444; text-decoration-thickness: 2px; }
    .overln { text-decoration: overline; text-decoration-color: #4f46e5; text-decoration-thickness: 2px; }

    /* ── text-transform ── */
    .tf-upper { text-transform: uppercase; letter-spacing: 0.08em; font-weight: 700; font-size: 0.8rem; color: #4f46e5; }
    .tf-lower { text-transform: lowercase; font-size: 1rem; }
    .tf-cap   { text-transform: capitalize; font-size: 1rem; }

    /* ── text-shadow ── */
    .shadow-soft  { font-size: 1.5rem; font-weight: 700; text-shadow: 2px 2px 8px rgba(0,0,0,0.2); }
    .shadow-glow  { font-size: 1.5rem; font-weight: 700; color: #818cf8; text-shadow: 0 0 20px rgba(79,70,229,0.6), 0 0 40px rgba(79,70,229,0.3); background: #1e293b; padding: 8px 16px; border-radius: 8px; }
    .shadow-hard  { font-size: 1.5rem; font-weight: 700; text-shadow: 3px 3px 0 #4f46e5; }

    /* ── Highlight ── */
    .highlight {
      background: linear-gradient(120deg, #fde68a 0%, #fde68a 100%);
      background-repeat: no-repeat;
      background-size: 100% 40%;
      background-position: 0 85%;
      padding-bottom: 2px;
    }
  </style>
</head>
<body>

  <h3>Custom Link Underlines</h3>
  <div class="demo-row">
    <a href="#" class="link-default">Browser default</a>
    <a href="#" class="link-custom">Custom offset + colour (hover me)</a>
    <a href="#" class="link-wavy">Wavy red underline</a>
    <a href="#" class="link-animated">Fade-in on hover</a>
  </div>

  <h3>Other Decoration Lines</h3>
  <div class="demo-row">
    <span class="strike">Was $99.00</span>
    <span class="overln">Overline text</span>
  </div>

  <h3>text-transform</h3>
  <div class="demo-row">
    <span class="tf-upper">uppercase label</span>
    <span class="tf-lower">LOWERCASE TEXT</span>
    <span class="tf-cap">capitalize each word</span>
  </div>

  <h3>text-shadow Variations</h3>
  <div class="demo-row">
    <span class="shadow-soft">Soft shadow</span>
    <span class="shadow-glow">Glow effect</span>
    <span class="shadow-hard">Hard offset</span>
  </div>

  <h3>Gradient Highlight</h3>
  <p style="font-size:1.1rem;line-height:1.7">
    CSS background gradients can simulate a <span class="highlight">marker highlight effect</span> on inline text without any extra markup.
  </p>

</body>
</html>

How It Works

Step 1 — text-underline-offset Separates the Line from Text

text-underline-offset: 4px moves the underline 4px below the text baseline. The default browser underline sits very close to the descenders — letters like g, p, y. An offset of 3–5px gives the underline breathing room and prevents it from cutting through descenders.

Step 2 — text-decoration-color: transparent Enables Hover Reveals

Setting the decoration colour to transparent keeps the underline present in the layout (avoiding text shifting on hover) but visually invisible. A CSS transition on text-decoration-color then smoothly reveals the underline on hover — a modern pattern that replaces the older border-bottom trick.

Step 3 — Wavy Decoration for Spelling Indicators

text-decoration-style: wavy mimics the red squiggly line of spell-checkers. Combined with a red colour and an appropriate text-decoration-line: underline, it creates a familiar visual cue for error states, wrong answers, or flagged content.

Step 4 — text-transform Preserves Semantic Text

The HTML contains “uppercase label” in mixed case. text-transform: uppercase renders it visually as “UPPERCASE LABEL” without changing the DOM. Screen readers read the original “uppercase label” — not shouted text — maintaining correct pronunciation.

Step 5 — Gradient Background Simulates Highlight

The highlight effect uses background: linear-gradient() on an inline span. The gradient is positioned at 85% vertically with a height of 40% — this places the yellow band behind the bottom portion of the text, simulating a marker stroke without any images or extra elements.

/* nav-links.css */
.nav { display: flex; gap: 4px; align-items: center; }

/* Base nav link */
.nav-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 14px;
  font-size: 0.875rem;
  font-weight: 500;
  color: #475569;
  text-decoration: none;        /* remove default browser underline */
  border-radius: 8px;
  position: relative;
  transition: color 0.15s, background 0.15s;
}

/* Hover state */
.nav-link:hover { color: #0f172a; background: #f1f5f9; }

/* Active link — underline indicator */
.nav-link.active {
  color: #4f46e5;
  font-weight: 600;
}
.nav-link.active::after {
  content: '';
  position: absolute;
  bottom: -1px; left: 14px; right: 14px;
  height: 2px;
  background: #4f46e5;
  border-radius: 2px 2px 0 0;
}

/* Badge on nav item */
.nav-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  background: #4f46e5;
  color: white;
  font-size: 0.65rem;
  font-weight: 700;
  border-radius: 9999px;
  line-height: 1;
}

/* Price tag — strikethrough old price */
.price-old {
  text-decoration: line-through;
  text-decoration-thickness: 1.5px;
  text-decoration-color: #94a3b8;
  color: #94a3b8;
  font-size: 0.9em;
}
.price-new {
  color: #0f172a;
  font-weight: 700;
}

Common Mistakes

❌ Wrong — links cannot be identified by colour-blind users:

a { color: #4f46e5; text-decoration: none; }
/* Fails WCAG 1.4.1 — relies on colour alone to identify links */

✅ Correct — retain underline or add another non-colour indicator:

a {
  color: #4f46e5;
  text-decoration-line: underline;
  text-decoration-color: #a5b4fc;
  text-underline-offset: 3px;
}
a:hover { text-decoration-color: #4f46e5; }

Mistake 2 — Using text-transform: capitalize for proper titles

❌ Wrong — capitalize only uppercases the first letter of each word, including articles:

.title { text-transform: capitalize; }
/* "the quick brown fox" becomes "The Quick Brown Fox" — "The" should be lowercase in title case */

✅ Correct — write true title case in HTML; use text-transform for labels only:

<h1>The Quick Brown Fox</h1> <!-- proper title case in markup -->

Mistake 3 — Heavy text-shadow slowing render performance

❌ Wrong — multiple large blur-radius shadows on many elements cause paint jank:

/* Applied to hundreds of list items: */
li { text-shadow: 0 0 30px rgba(0,0,0,0.5), 0 0 60px rgba(0,0,0,0.3), 0 4px 12px rgba(0,0,0,0.4); }

✅ Correct — use text-shadow sparingly on hero/heading elements only:

.hero-title { text-shadow: 0 2px 8px rgba(0,0,0,0.3); }

▶ Try It Yourself

Quick Reference

Property Key Values Notes
text-decoration-line underline, overline, line-through, none Can combine: underline overline
text-decoration-color Any colour or transparent transparent enables hover reveal
text-decoration-style solid, dotted, dashed, wavy, double wavy good for error states
text-decoration-thickness 2px, 0.1em, from-font from-font uses metrics from font data
text-underline-offset 3–5px or 0.2em Separates line from descenders
text-transform uppercase, lowercase, capitalize, none Visual only — HTML unchanged
text-shadow x y blur colour Use sparingly — performance cost

🧠 Test Yourself

A “SALE” label in HTML reads <span>sale</span> in lowercase. How should you make it visually uppercase while keeping the HTML lowercase for screen readers?





▶ Try It Yourself