A Closer Look at MDN’s Fade-Out Text Trick

When MDN rolled out its redesign earlier this year, the new look came with a few CSS details worth stealing. One of the more subtle touches is how card components handle truncated text: instead of a hard cut or an ellipsis, the copy simply fades out at the bottom of the card.

It’s a neat effect for a couple of reasons. For one, it’s a deliberate use of CSS data loss—generally something to avoid, but perfectly fine here since excerpts are meant to tease the full content. It also sidesteps the main criticism leveled at text-overflow: ellipsis: once text is visually cut off, sighted users have no way to recover it. The fade approach keeps the truncation purely visual, which gives you more control over the experience.

The Setup

The HTML is about as simple as it gets: a card container wrapping a single paragraph.

<div class="card">
  <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Inventore consectetur temporibus quae aliquam nobis nam accusantium, minima quam iste magnam autem neque laborum nulla esse cupiditate modi impedit sapiente vero?</p>
</div>

Add a few baseline styles and the goal is to cut the content off after roughly the third line. That means a max-height on the paragraph with hidden overflow:

.card p {
  max-height: calc(4rem * var(--base)); /* Set a cut-off point for the content */
  overflow: hidden; /* Cut off the content */
}

Notice the calc() usage. A --base variable is defined up front and used as a common multiplier for the font-size, line-height, and card padding, as well as the paragraph’s max-height. Working from a constant value keeps the sizing proportional, and MDN appears to do something similar with its own --base-line-height variable.

Fading the Bottom Line

Creating the fade is a classic linear-gradient() on the paragraph’s :after pseudo-element, positioned at the bottom-right of the card:

.card p:after {
  content: ""; /* Needed to render the pseudo */
  background-image: linear-gradient(to right, transparent, var(--background) 80%);
  position: absolute;
  inset-inline-end: 0; /* Logical property equivalent to `right: 0` */
}

The gradient uses a --background variable set to the same color as the card’s background. That makes the text visually blend into the card itself. One tweak worth noting: the second color stop at 80% rather than 100%. Going all the way to opaque left a bit of text visible; 80% hides it cleanly.

The pseudo-element needs explicit width and height. The height uses --base again so it scales with the paragraph’s line-height and fully covers the text:

.card p:after {
  /* same as before */
  height: calc(1rem * var(--base) + 1px);
  width: 100%; /* relative to the .card container */
}

An extra pixel of height was needed here, though MDN’s implementation gets away without it—they don’t offset the pseudo-element with top or inset-block-start, which apparently makes the difference.

Since p:after is absolutely positioned, the paragraph needs explicit relative positioning to keep the pseudo-element in the flow:

.card p {
  max-height: calc(4rem * var(--base)); /* Set a cut-off point for the content */
  overflow: hidden; /* Cut off the content */
  position: relative; /* needed for :after */
}

The Missing display

Even with all that in place, the gradient may still appear in the wrong spot. The culprit: :after needs to be displayed as a block element. Adding a red border makes the issue obvious:

.card p:after {
  content: "";
  background: linear-gradient(to right, transparent, var(--background) 80%);
  display: block;
  height: calc(1rem * var(--base) + 1px);
  inset-block-end: 0;
  position: absolute;
  width: 100%;
}

With that fixed, the full effect comes together:

VoiceOver reads the full text, even though it’s visually truncated. Other screen readers were not tested.

One last detail: MDN’s version removes pointer-events from p:after. It’s a defensive move that prevents odd behavior when selecting text, and it does make text selection feel smoother in Safari, Firefox, and Chrome.