Skip to main content

Harshal V. LADHE

Colouring a CSS text-overflow Ellipsis Separately from Its Text

Give truncated text's ellipsis a colour of its own.
Published at:
Last updated:
Estimated reading time:11 min read

Introduction

text-overflow: ellipsis is how a long title, file name or table cell shows that it's been cut short: the text stops and three dots — "…" — take the place of what didn't fit. It's one of the most common lines of CSS on the web, and most of the time nobody thinks about what colour those dots are.

Then a design asks for something slightly different. The title is dark grey, but the dots should be the brand's accent colour, so they read as a "there's more here" hint rather than part of the text. Or the title is a link, and the dots come out in the wrong colour without anyone asking them to.

There's no ::ellipsis pseudo-element and no ellipsis-color property, so it can look as if the dots are out of reach. They aren't. The ellipsis takes its colour from one specific element, and once you know which one, colouring it separately from the text is a two-line change — no extra markup, no JavaScript and no fake dots.

The Three Rules Every Ellipsis Needs

Before colouring an ellipsis, it helps to know exactly what makes one appear. text-overflow does nothing on its own; it only decides what to show when text overflows a box. Three other conditions have to be true first:

.title {
  inline-size: 16rem;      /* 1. the box has a limited width */
  white-space: nowrap;     /* 2. the text stays on one line */
  overflow: hidden;        /* 3. whatever doesn't fit is clipped */
  text-overflow: ellipsis; /* ...and the clipped edge shows "…" */
}
  1. A limited width. A fixed inline-size, a max-inline-size, or a parent that constrains it. Without one, the box just grows to fit the text and nothing ever overflows.
  2. No wrapping. By default, text wraps onto a new line instead of overflowing. white-space: nowrap keeps it on one line so it has to run past the edge.
  3. Clipping. overflow must be something other than visible — usually hidden, or clip. Visible overflow isn't cut off, so there's nothing for an ellipsis to mark.

If any of the three is missing, text-overflow: ellipsis is silently ignored. That's the first thing to check whenever an ellipsis doesn't show up.

Why the Dots Ignore Your Text Colour

Card titles are rarely a bare line of text. More often the text sits inside an inline element — a <span>, an <a> or a <strong> — inside the box that does the clipping:

<h3 class="title">
  <a href="/posts/long-post">A post title far too long to fit on one line</a>
</h3>

Give the link its own colour, truncate the heading, and the dots won't match the link. They match the heading.

That's by design. The CSS specification says the ellipsis is styled according to the block container — the element that has text-overflow on it — not the inline element whose text happens to be cut off. The browser draws the "…" as if it were a piece of text belonging directly to that container, so it uses the container's color, font and other text styles.

Colouring the Ellipsis

That rule is exactly what makes a separate colour possible. Set the ellipsis colour on the container, then set the text colour on an inline element inside it:

<h3 class="title">
  <span class="title-text">A card title far too long to fit on one line</span>
</h3>
.title {
  inline-size: 16rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  color: #f97316; /* the ellipsis colour */
}

.title-text {
  color: #1f2937; /* the text colour */
}

Every character of the title is inside .title-text, so it's drawn in dark grey. The only thing left that belongs to .title itself is the ellipsis, so the ellipsis is drawn in orange.

This is still the browser's own, native ellipsis. It appears only when the text actually overflows, disappears when it fits, sits on the correct side in right-to-left languages, and is never selected or copied with the text. All you've changed is its colour.

Custom Properties Make It Reusable

To use the same pattern across a design system, expose both colours as custom properties with sensible defaults:

.truncate {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--ellipsis-colour, currentColor);
}

.truncate > span {
  color: var(--text-colour, inherit);
}

/* Opt in where a design needs it */
.card-title {
  --ellipsis-colour: var(--brand-accent);
}

Unless a component sets --ellipsis-colour, the dots and the text inherit the same colour, so the default looks like an ordinary ellipsis.

It's Not Just the Colour

Because the ellipsis takes all of its text styles from the container, the same rule applies to font-weight, font-size, font-family and letter-spacing. If the inner span is bold and the container isn't, the title is bold but the dots are thin:

/* ❌ Bold title, thin dots */
.title-text {
  font-weight: 700;
}

/* ✅ Bold title, bold dots */
.title {
  font-weight: 700;
}

The rule of thumb: put everything the text and the dots should share — font, size, weight — on the container, and put only what should differ — usually just color — on the inner element.

Truncating Inside Flex and Grid Layouts

Truncation breaks most often inside flexbox and grid, and there are two separate reasons.

text-overflow has to go on the item, not the layout. A flex or grid container isn't a block container — it lays out boxes, not lines of text — so text-overflow on it has no effect. Put the truncation rules on the item that holds the text.

A wrapper around the title won't shrink below its content. A card usually puts the title inside a wrapper — a .card-body holding the title and a line of meta text — and makes that wrapper the flex item. By default, a flex item's minimum width is the width of its content, which for a no-wrap title is the whole line. The wrapper grows to fit, the title inside it never runs out of room, and no ellipsis appears. min-inline-size: 0 on the wrapper lets it shrink:

<article class="card">
  <div class="card-body">
    <h3 class="title"><span>A card title far too long to fit on one line</span></h3>
    <p>12 min read</p>
  </div>
  <span class="badge">Tutorial</span>
</article>
.card {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

.card-body {
  flex: 1;
  min-inline-size: 0; /* allow shrinking below the title's width */
}

.title {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  color: #f97316;
}

.title > span {
  color: #1f2937;
}

When the truncating element is itself the flex item, it doesn't need this: a flex item with overflow: hidden already has a minimum width of 0. The trap is always a wrapper with ordinary, visible overflow between the flex container and the text.

In a grid, the same applies to tracks: use a track size that can shrink, such as minmax(0, 1fr), rather than plain 1fr, which has a content-sized minimum (see minmax() overflowing anyway).

Multi-line Truncation with line-clamp

text-overflow only works on a single line. To show two or three lines and end the last one with an ellipsis, use -webkit-line-clamp:

.excerpt {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  color: #f97316; /* the ellipsis colour */
}

.excerpt > span {
  color: #1f2937; /* the text colour */
}

Despite the -webkit- prefix, this works in every current browser, and it remains the dependable way to clamp lines today. The unprefixed line-clamp property is part of the CSS Overflow specification, but support for it is still patchy, so use the prefixed version.

The ellipsis is still drawn by the clamped box, so the same colouring technique works here too: the box sets the dots' colour, and the inline span sets the text's.

Try It Live

The sandbox shows three cards, each a flex row with a body — a title of a different length and a reading time — and a badge beside it. Drag Card width to see each title truncate and recover — the dots appear only on the titles that actually overflow. Switch Ellipsis colour between Same as text and Accent to compare the ordinary ellipsis with a coloured one, and switch Lines to clamp the titles to two lines instead of one. Untick min-inline-size: 0 to see the flex pitfall: the card bodies stop shrinking, the ellipsis disappears and the badges are pushed out of the cards. The readout shows the CSS behind the current state and how many titles are truncated, worked out with the JavaScript check described in the next section. Hover over a truncated title to see its full text.

Coloured Ellipsis Playground

Colour a native text-overflow ellipsis separately from its text, resize the cards to see which titles truncate, clamp them to two lines, and see a flex item without min-inline-size: 0 refuse to shrink.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Coloured Ellipsis Playground</title>
  </head>
  <body>
    <main class="playground">
      <header class="playground-header">
        <h1>Colouring the text-overflow Ellipsis</h1>
        <p>The ellipsis takes its colour from the box that truncates, not from the text inside it. Each title below sets the dots' colour on the heading and the text's colour on a span inside it. Narrow the cards to see which titles truncate, then untick <code>min-inline-size: 0</code> to see the flex item around a title refuse to shrink.</p>
      </header>

      <fieldset class="control-group">
        <legend>Ellipsis colour</legend>
        <div class="segmented-control" role="radiogroup" aria-label="Ellipsis colour">
          <label class="segment"><input type="radio" name="colour" value="text" checked><span>Same as text</span></label>
          <label class="segment"><input type="radio" name="colour" value="accent"><span>Accent</span></label>
        </div>
      </fieldset>

      <fieldset class="control-group">
        <legend>Lines</legend>
        <div class="segmented-control" role="radiogroup" aria-label="Lines">
          <label class="segment"><input type="radio" name="lines" value="1" checked><span>1 line (text-overflow)</span></label>
          <label class="segment"><input type="radio" name="lines" value="2"><span>2 lines (line-clamp)</span></label>
        </div>

        <div class="field">
          <label for="width-input">Card width</label>
          <div class="range-row">
            <span class="range-edge">10rem</span>
            <div class="range-track">
              <input type="range" id="width-input" min="10" max="30" step="1" value="18">
              <output class="range-bubble" id="width-value" for="width-input">18rem</output>
            </div>
            <span class="range-edge">30rem</span>
          </div>
        </div>

        <label class="checkbox-field">
          <input type="checkbox" id="shrink-toggle" checked>
          <code>min-inline-size: 0</code> on the card body (a flex item)
        </label>
      </fieldset>

      <section class="demo-area" aria-label="Truncated card titles">
        <ul class="cards" id="cards">
          <li class="card">
            <div class="card-body">
              <h2 class="card-title"><span>Building an Accessible Date Picker Without a Single Dependency</span></h2>
              <p class="card-meta">12 min read</p>
            </div>
            <span class="card-badge">Tutorial</span>
          </li>
          <li class="card">
            <div class="card-body">
              <h2 class="card-title"><span>Container Queries in Practice</span></h2>
              <p class="card-meta">7 min read</p>
            </div>
            <span class="card-badge">Post</span>
          </li>
          <li class="card">
            <div class="card-body">
              <h2 class="card-title"><span>CSS Grid</span></h2>
              <p class="card-meta">2 min read</p>
            </div>
            <span class="card-badge">Snippet</span>
          </li>
        </ul>
      </section>

      <output class="readout" id="readout" aria-live="polite"></output>
    </main>

    <script src="./index.js"></script>
  </body>
</html>

Read-only
Ln –, Col –HTML3.3 KBUTF-8

Starting sandbox…

No console output yet.

Detecting Truncation with JavaScript

CSS doesn't tell you whether an ellipsis is showing, but the element's sizes do. If its content is wider than its visible box, it's been truncated:

function isTruncated(element) {
  return element.scrollWidth > element.clientWidth;
}

For line-clamp, compare scrollHeight with clientHeight instead, since the extra content overflows downwards.

This is useful for showing the full text only when it's hidden — a title attribute or a tooltip on truncated titles, and nothing on titles that fit. Because truncation depends on the available width, re-run the check when the element is resized, with a ResizeObserver.

Accessibility

The full text is still there. Truncation is purely visual. Screen readers read the whole title, search engines index it, and copying it copies all of it. That's a real advantage over shortening the string itself on the server.

Sighted users lose the end of the text. Anyone who can see the page but can't hover — touch-screen and keyboard users — has no way to read a truncated title, and a title attribute doesn't help them, since it only appears on mouse hover. Where the hidden part matters, link to a page that shows it in full, or reveal it on focus with a tooltip like the one in CSS-Only Tooltips with attr() and content. Attach the tooltip to a wrapper rather than the title itself, since the title's overflow: hidden would clip a tooltip drawn inside it.

Keep the dots visible. If the ellipsis is the only sign that something has been cut off, an accent colour that barely stands out from the background hides that sign. Aim for at least a 3:1 contrast ratio between the dots and the background behind them.

Browser Support

text-overflow: ellipsis has worked in every browser for well over a decade. Colouring the ellipsis through its container isn't a newer feature — it's simply how the specification defines the ellipsis, and Chrome, Edge, Firefox and Safari all draw it with the container's styles.

-webkit-line-clamp works in all current browsers. A few related features have narrower support:

FeatureSupport
text-overflow: ellipsisAll browsers
Ellipsis styled by its containerAll browsers
-webkit-line-clampAll current browsers
text-overflow: "<string>" (custom)Firefox only
Two values (text-overflow: ellipsis ellipsis)Firefox only
Unprefixed line-clampLimited — use the prefixed version

Common Pitfalls

1. Putting text-overflow on the Flex Container

display: flex on the element with text-overflow turns it into a flex container rather than a block container, and the ellipsis never appears. Truncate the item that holds the text, not its flex or grid parent.

2. Forgetting min-inline-size: 0

A flex item that wraps the title, without min-inline-size: 0, grows to the full width of the title and pushes its siblings out of the way instead of letting the title truncate. If a flex layout overflows its container rather than showing an ellipsis, look for a wrapper between the flex container and the text — it almost always needs min-inline-size: 0.

3. Making the Inner Element a Block

The technique relies on the text being inline content of the truncating container. Give the inner span display: block or inline-block and it becomes a box of its own: the container no longer has a line of text to truncate, so the ellipsis disappears and the title is simply cut off mid-letter. Keep the inner element inline.

4. Faking the Dots with an Overlay

A common workaround is to clip the text and absolutely position a <span>…</span> over its end, with a background that hides the cut-off characters. It looks right in a screenshot, but the fake dots are there whether or not the text overflows, so short titles end in "…" too. Its background has to match whatever is behind it, which breaks on gradients, images and theme changes, and it has to be repositioned for right-to-left text. The native ellipsis, styled through its container, has none of these problems.

Key Takeaway

The ellipsis from text-overflow is styled by the block container that truncates, not by the text it cuts off. Put the ellipsis colour — along with the font both should share — on the container, wrap the text in an inline element with its own colour, and the dots can be any colour you like while staying the browser's real, native ellipsis. The same approach works for multi-line -webkit-line-clamp. In flex and grid layouts, remember to truncate the item rather than the container, and to give any wrapper around the text min-inline-size: 0.

Changelog

  • — Added cross-references to post 'Responsive CSS Grid Layouts: `fr` Units, `minmax()`, `auto-fill`, and `auto-fit` Explained' and tutorial 'CSS-Only Tooltips with `attr()` and `content`'
  • — Initial publication.
This tutorial is licensed under CC BY 4.0 by the author.

Share this tutorial

Spotted a mistake or something outdated in this tutorial? Suggest a correction.

  • Theming Native Controls with accent-color

    Theme native checkboxes, radios and sliders with one property.
    The accent-color property themes the checked and filled parts of native checkboxes, radio buttons, switches, range sliders and progress bars — including their indeterminate states — while keeping real browser controls with full native behaviour and accessibility, plus how it pairs with color-scheme and what it leaves alone.
    Published at:
  • Styling a <select> Dropdown with CSS appearance: base-select

    Style a real select's dropdown, options and arrow.
    appearance: base-select opts a native <select> into a fully styleable mode — rich HTML in options, <selectedcontent> in the closed button, and ::picker(select), ::picker-icon and ::checkmark for the dropdown, arrow and tick — while keeping its keyboard support, accessibility and form behaviour, with a plain select as the fallback.
    Published at:
  • A Modern CSS Reset, Explained Rule by Rule

    A small, modern reset, and the reason behind every line.
    A compact modern CSS reset, explained rule by rule: border-box sizing, margin removal, keyword animations with interpolate-size, readable line-height, block-level media, form controls that inherit your font, overflow-wrap and text-wrap for better line breaks, and an isolated stacking context — plus optional extras and the edge cases to watch for.
    Published at: