Text decoration in CSS: every line, style and offset

text-decoration draws lines under, over or through text. Four longhands set which line, its style, colour and thickness, and two more properties move the underline and control how it meets the letters.

text-decoration is the CSS property that draws a line under, over or through text. The shorthand takes the line, style, colour and thickness in any order. For example, text-decoration: underline wavy #e11d48 2px; gives a thin red wave under the text.

Try every combination below. Change the controls and the box prints the CSS to copy.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>text-decoration builder</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .preview {
    background: #fff; border-radius: 12px; padding: 22px 16px;
    font-size: 30px; line-height: 1.8; text-align: center;
  }
  form { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 10px 16px; margin: 14px 0; font-size: 14px; }
  fieldset { border: 0; padding: 0; margin: 0; }
  legend, .lbl { font-weight: 600; margin-bottom: 4px; display: block; }
  label { display: block; }
  select, input[type=range] { width: 100%; }
  pre {
    margin: 0; padding: 12px 14px; border-radius: 10px;
    background: #1d2330; color: #e5e7eb; font-size: 13px; white-space: pre-wrap;
  }
</style>
</head>
<body>
<div class="preview"><span id="text">Typography, quietly</span></div>

<form id="f">
  <fieldset>
    <legend>Line</legend>
    <label><input type="checkbox" name="line" value="underline" checked> underline</label>
    <label><input type="checkbox" name="line" value="overline"> overline</label>
    <label><input type="checkbox" name="line" value="line-through"> line-through</label>
  </fieldset>
  <div>
    <label class="lbl" for="style">Style</label>
    <select id="style">
      <option>solid</option><option>double</option><option>dotted</option>
      <option>dashed</option><option selected>wavy</option>
    </select>
    <label class="lbl" for="color" style="margin-top:8px">Colour</label>
    <input type="color" id="color" value="#e11d48">
  </div>
  <div>
    <label class="lbl" for="thick">Thickness: <span id="tv"></span></label>
    <input type="range" id="thick" min="0" max="8" value="2">
    <label class="lbl" for="offset">Underline offset: <span id="ov"></span></label>
    <input type="range" id="offset" min="0" max="12" value="4">
    <label><input type="checkbox" id="skip"> draw through descenders</label>
  </div>
</form>

<pre id="out"></pre>

<script>
  const text = document.getElementById('text');
  const f = document.getElementById('f');
  const $ = (id) => document.getElementById(id);

  function update() {
    const lines = [...f.querySelectorAll('[name=line]:checked')].map((c) => c.value);
    const t = $('thick').value;  // 0 means "auto": let the browser choose
    const rules = {
      'text-decoration-line': lines.join(' ') || 'none',
      'text-decoration-style': $('style').value,
      'text-decoration-color': $('color').value,
      'text-decoration-thickness': t === '0' ? 'auto' : t + 'px',
      'text-underline-offset': $('offset').value + 'px',  // only moves the underline
      'text-decoration-skip-ink': $('skip').checked ? 'none' : 'auto',
    };
    text.removeAttribute('style');
    for (const [prop, val] of Object.entries(rules)) text.style.setProperty(prop, val);

    $('tv').textContent = rules['text-decoration-thickness'];
    $('ov').textContent = rules['text-underline-offset'];
    $('out').textContent =
      '.fancy {\n' + Object.entries(rules).map(([p, v]) => `  ${p}: ${v};`).join('\n') + '\n}';
  }

  f.addEventListener('input', update);
  update();
</script>
</body>
</html>
Pick lines, a style, a colour, a thickness and an offset. The CSS under the preview updates as you go.

Only the underline moves with the offset slider. Overline and line-through stay where the font puts them.

The four longhands and the shorthand

The shorthand is a short way of writing four separate properties. Each one can also be set on its own, which is handy for hover states that change a single part.

Property Values Default
text-decoration-line none, underline, overline, line-through, or several none
text-decoration-style solid, double, dotted, dashed, wavy solid
text-decoration-color any colour currentColor (the text colour)
text-decoration-thickness auto, from-font, a length or a percentage auto
The parts of a decoration: which line, its style, colour and thickness, and the offset from the baseline.
The parts of a decoration: which line, its style, colour and thickness, and the offset from the baseline.

Two related properties are not part of the shorthand. text-underline-offset sets the gap between the underline and the text. text-decoration-skip-ink decides whether the line breaks around letters that hang below the baseline.

.note {
  text-decoration: underline dotted #0f766e 2px;
  text-underline-offset: 0.25em;
  text-decoration-skip-ink: none; /* draw through g, p, y */
}

Offset and thickness: making underlines readable

A default underline sits close to the letters and can touch them. text-underline-offset opens a gap. Use an em value such as 0.2em so the gap grows and shrinks with the font size.

Thickness works the same way. auto and from-font let the browser or the font file decide, so the same rule can look heavier in one font than another. A length such as 2px or 0.08em gives a fixed result in every font.

Why a child cannot remove its parent's underline

This one is easy to walk into. A paragraph is underlined, and one span inside it gets text-decoration: none. The span stays underlined.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Why a child cannot remove its parent's underline</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .case { background: #fff; border-radius: 12px; padding: 12px 16px; margin-bottom: 12px; }
  .case h3 { margin: 0 0 4px; font-size: 13px; color: #5b6270; font-weight: 600; }
  code { font-size: 12px; background: #eef1f5; border-radius: 4px; padding: 0 4px; }

  /* The parent draws the underline */
  .parent { font-size: 24px; margin: 6px 0; text-decoration: underline 3px #e11d48; }

  /* Case 1: does NOT work. The span is still inside the parent's underline */
  .try-none { text-decoration: none; color: #0f766e; }

  /* Case 2: works. An inline-block is not decorated by its parent */
  .fix { display: inline-block; text-decoration: none; color: #0f766e; }

  /* Case 3: the child draws its own line over the parent's */
  .cover { text-decoration: underline 3px #0f766e; color: #0f766e; }
</style>
</head>
<body>
<div class="case">
  <h3>1. Child has <code>text-decoration: none</code></h3>
  <p class="parent">Parent text <span class="try-none">child span</span></p>
</div>

<div class="case">
  <h3>2. Child is also <code>display: inline-block</code></h3>
  <p class="parent">Parent text <span class="fix">child span</span></p>
</div>

<div class="case">
  <h3>3. Child draws its own underline on top</h3>
  <p class="parent">Parent text <span class="cover">child span</span></p>
</div>
</body>
</html>
Case 1 still shows the line. Case 2 escapes it with inline-block. Case 3 paints its own line on top.

The line belongs to the parent. The parent draws it across all of its inline content, children included. The child's none only means the child adds no line of its own.

Left: none on the child changes nothing. Right: an inline-block child is left out of the parent's line.
Left: none on the child changes nothing. Right: an inline-block child is left out of the parent's line.

There are three ways out.

Make the child display: inline-block. Decorations are not carried into inline-block children, floats or absolutely positioned elements.

Draw over it. Give the child its own underline in another colour. It is painted on top of the parent's line, so with the same font and a thickness at least as large it hides it.

Move the decoration. Put the underline only on the parts that need it, not on the whole parent. This is usually the cleanest fix.

Strikethrough: s, del or CSS

A line through text can carry meaning, so pick the element before the style. Browsers draw line-through on both <s> and <del> by default.

Use For Example
<s> No longer accurate or relevant An old price
<del> Removed in an edit of the document A changed date in minutes
line-through in CSS Visual only, no meaning A finished to-do item

Screen readers may not announce a strikethrough at all. For prices, add words such as "Was" and "now", hidden visually if you like, so the old price is not read as the current one.

<p class="price">
  <s><span class="sr">Was </span>$49</s> $29
</p>

The <strike> element is obsolete. Use <s> or <del> instead.

Inside running text, a link needs something besides colour to stand out, because some readers cannot tell the colours apart. WCAG, the web accessibility guideline, makes the same point in its rule on use of colour.

You can still make the underline quiet. A thin, lighter line with a small offset that thickens on hover and focus reads as a link without shouting.

Removing it entirely is covered in HTML link without underline, and the hover colour order in HTML link hover color.

An underline that grows on hover

Transitions on the decoration itself are limited: you can fade the colour or slide the offset. For a line that grows from left to right, draw it as a background instead.

The underline is a background image. Its width goes from 0% to 100% on hover.
The underline is a background image. Its width goes from 0% to 100% on hover.

A linear-gradient of one colour makes a solid bar. background-size sets it to 0% wide and 2px tall, pinned to the bottom left. On hover the width goes to 100%, and transition animates the change.

The finished example puts it all together: quiet underlines for links in text, growing underlines for navigation, a struck-through price and a spelling-style wave.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Text decoration in a real page</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; line-height: 1.6; }
  .box { background: #fff; border-radius: 12px; padding: 14px 16px; margin-bottom: 12px; }
  h3 { margin: 0 0 6px; font-size: 13px; color: #5b6270; }

  /* 1. Links in text: a quiet underline pushed below the letters */
  .prose a {
    color: #1d4ed8;
    text-decoration: underline 1px;     /* colour follows the link colour */
    text-underline-offset: 0.2em;
    text-decoration-color: rgba(29, 78, 216, .45);
  }
  .prose a:hover, .prose a:focus-visible {
    text-decoration-thickness: 2px;       /* thicker on hover and keyboard focus */
    text-decoration-color: currentColor;
  }

  /* 2. Navigation: an underline that grows from the left */
  nav { display: flex; gap: 18px; flex-wrap: wrap; }
  nav a {
    color: #1d2330; font-weight: 600; text-decoration: none;
    background: linear-gradient(currentColor, currentColor) left bottom / 0% 2px no-repeat;
    transition: background-size .25s;
    padding-bottom: 2px;
  }
  nav a:hover, nav a:focus-visible, nav a[aria-current] { background-size: 100% 2px; }

  /* 3. Prices: the old one struck through */
  .price { font-size: 26px; font-weight: 700; }
  .price s { font-size: 16px; font-weight: 400; color: #6b7280; text-decoration-thickness: 2px; }
  .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }

  /* 4. A spelling-style mark */
  .typo { text-decoration: underline wavy #dc2626; text-decoration-skip-ink: none; text-underline-offset: 3px; }
</style>
</head>
<body>
<div class="box">
  <h3>Links</h3>
  <nav>
    <a href="#home" aria-current="page">Home</a>
    <a href="#pricing">Pricing</a>
    <a href="#help">Help</a>
  </nav>
  <p class="prose">Read the <a href="#guide">setup guide</a> first, then check the <a href="#faq">questions page</a> if something looks off.</p>
</div>

<div class="box">
  <h3>Price</h3>
  <p class="price"><s><span class="sr">Was </span>$49</s> $29 <span class="sr">now</span></p>
</div>

<div class="box">
  <h3>Spelling mark</h3>
  <p>The meeting is on <span class="typo" title="Did you mean Wednesday?">Wensday</span> at ten.</p>
</div>
</body>
</html>
Hover or tab through the links. The navigation underline grows, and the text links thicken.

For effects that need extra shapes, such as a double bar or a highlight behind the text, ::before and ::after add them without extra markup.

When it does not work

What you see Cause Fix
A child still has the underline after none The line is drawn by an ancestor display: inline-block on the child, or move the decoration
The underline touches or cuts the letters Default offset for that font text-underline-offset: 0.2em
Thickness looks different in another font auto or from-font follow the font Set a length such as 2px
The colour or thickness you set disappeared A later text-decoration shorthand reset them Put the shorthand first, longhands after
Offset has no effect The line is an overline or line-through Offset moves only the underline
Links look like plain text Underline removed and colour is the only cue Keep a thin underline, or add weight or an icon

The shorthand trap is common. Writing text-decoration: underline after text-decoration-color: red sets the colour back to its default.

Decoration details are hard to judge from a description. A wave, an offset or a hover animation shows up only when someone sees the real page, at their own font size.

To send the working version, paste the page into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written and its scripts run, so the people you send it to can hover the links and try the builder themselves. If you change the code later, the same link shows the new version.

Questions people ask

What does text-decoration: none do?

It removes decoration lines that the element itself would draw, which is why it takes the default underline off a link. It cannot remove a line drawn by an ancestor, because that line belongs to the parent and is only painted across the child.

How do I make a wavy underline in CSS?

text-decoration: underline wavy red; sets the line, the style and the colour in one declaration. Add text-underline-offset to push the wave below the letters and text-decoration-skip-ink: none if you want the wave unbroken under g, p and y.

How do I move an underline further from the text?

Use text-underline-offset, for example 0.2em or 4px. It moves only the underline, not an overline or line-through. An em value keeps the gap in proportion when the font size changes.

Should I use s, del or CSS for a strikethrough?

Use s for something that is no longer accurate, such as an old price. Use del for text removed in an edit of the document. Use text-decoration: line-through for a purely visual effect with no meaning, such as a finished item in a styled list.

Can I animate an underline?

text-decoration-color and text-underline-offset can be transitioned. For an underline that grows across the word, draw it as a background gradient and transition background-size from 0% to 100%.

Keep reading