How do you create a highlighting effect that overflows the text content?

Viewed 88

I'm trying to create a highlighting effect which leaves a bit of space at the front and at the end for legibility, to mimic how you'd highlight a book. Here are a couple of attempts:

p {
  width: 10em;
}

mark.with-padding {
  border-radius: 10px;
  padding: 0.4em 0.2em;
  background-color: hsl(200deg 100% 80%);
  box-decoration-break: clone;
}

mark.with-before-after {
  border-radius: 10px;
  padding: 0.4em 0px;
  background-color: hsl(200deg 100% 80%);
  box-decoration-break: clone;
}
mark.with-before-after::before {
  content: '\2009';
}
mark.with-before-after::after {
  content: '\2009';
}
<p>Some text here.&ThinSpace;<mark class="with-padding">This text is highlighted</mark></p>

<p>Some text here.&ThinSpace;<mark class="with-before-after">This text is also highlighted</mark></p>

  1. Using padding along with box-decoration-break: clone; applies padding on soft line breaks too, which I like. However, the vertical alignment of text is understandably not quite right, because the padding takes up some space for lines which have highlighting at the start.
  2. Using ::before + ::after with a thin space ensures vertical alignment for lines which begin with highlighting vs without. However, this means that there is no gap between the highlight and the text on a new line.

With a physical book, the text is normally left-aligned and highlighting "overflows" outside of that boundary. So it's a mix of 1 + 2, you have both padding (depending on where you start highlighting) and vertical alignment.

How do you create such an effect with pure CSS (no JS)?

See below for an approximate example of the desired result, which I manually drew in Notability:

text highlighting in Notability

This has three properties:

  1. Line-wrapping works (like an inline element).
  2. The text is properly justified; the left edge of the text for the highlighted line ('t') matches the left edge of unhighlighted line ('p').
  3. There is "padding" on the highlights before and after the text, on every line.

The combination of these properties means that the highlight must "overflow" outside the left/right boundaries of the text content.

2 Answers

Try copying the text, highlight the copied text, and make it transparent. Then superimpose the invisible highlighted copied text to the visible text.

This solution requires that the a default font is established and that the width of text is explicitly set as it is in the OP code (Original Post aka the question) (see Figure I).

Figure I

html { font: 300 2ch/2.25ex 'Segoe UI'; }
p { width: 11em; }

Next, wrap the visible text with a <div> and assign a class to it (ex. 'anchor'). Then add a <p> to the <div> and assign a class to it (ex. 'ghost'). Copy the text of the <div> and place it in the <p>. Then wrap the part of the copied text that is supposed to be highlighted with a <mark>. (see Figure II).

Figure II

<div class='anchor'>Copy this text. Highlight this part.
<p class='ghost'>Copy this text. <mark>Highlight this part.</mark></p>
</div>

The following are notes concerning the CSS units of measurement:

Figure III

Unit Description
em A length relative to the font-size of the parent element. In the example the default font-size is set by the html, so 1em = 2ch.
ex A length relative to the height of a "x" (which varies depending on the font-family) or 0.5em. In the example, ex is used for vertical padding, line-height, and positioning.
ch A length relative to the width of a "0" (which varies depending on the font-family). In the example, ch is used for horizontal padding and positioning.

Details are commented in example

html {
  font: 300 2ch/2.25ex 'Segoe UI'
}

p {
  width: 11em;
}

.anchor {
  /* 
  This establishes the <div> as the area for <p class='ghost'> to position 
  itself in.
  */
  position: relative;
  width: 11em;
  margin: 1em 0;
}

.ghost {
  /*
  Allows <p> to position itself within the perimeter of it's parent
  <div class='anchor'>.
  */
  position: absolute;
  /*
  Places <p> to occupy the layer underneath <div>.
  */
  z-index: -1;
  /*
  Shifts <p> down 
  */
  bottom: -2.5ex;
  /*
  Shifts <p> to the left
  */
  left: -0.5ch;
  /*
  <p> should be slightly wider than <div class='anchor'>
  */
  width: 12em;
  /*
  Hides the text
  */
  color: transparent;
}

mark {
  /*
  This establishes <mark> as the area for <mark>::before pseudo-element to position to
  */
  position: relative;
  /*
  Positions <mark> underneath <p>
  */
  z-index: -2;
  padding: 1ex 0.5ch 0 0;
  border-radius: 10px;
  /*
  Should be smaller than default line-height set at root to compensate for the
  extra padding
  */
  line-height: 1.25ex;
  /*
  Hides the text
  */
  color: transparent;
  background-color: hsl(200deg 100% 80%);
  /*
  Shapes the "box" as a continuous shape when wrapped. Needs vendor prefix
  for Chrome and Safari
  */
  box-decoration-break: slice;
  -webkit-box-decoration-break: slice;
}

mark::before {
  content: '\2009';
  /*
  Allows <::mark> to position itself within the perimeter of it's parent
  <mark>
  */
  position: absolute;
  /*
  Positions <::mark> underneath <mark>
  */
  z-index: -3;
  /*
  Keeps <::mark> at the top edge of <mark>
  */
  top: 0;
  /*
  Shifts <::mark> to the left.
  */
  left: -0.5ch;
  width: 6ch;
  padding: 1ex 0 1ex 1ch;
  border-top-left-radius: 10px;
  border-bottom-left-radius: 10px;
  /*
  Should be relatively smaller than the line-height of root because of 
  it's large padding. <::mark> is an extension of <mark> when it wraps over 
  to the next line. It also blends in if there is no wrapping.
  */
  line-height: 1.65ex;
  background-color: hsl(200deg 100% 80%);
}

mark::after {
  content: '\a0\a0';
  /*
  Allows <mark::> to position itself within the perimeter of it's parent
  <mark>
  */
  position: absolute;
  /*
  Positions <mark::> underneath <::mark>
  */
  z-index: -4;
  /*
  Keeps <::mark> at the top edge of <mark>
  */
  top: 0;
  /*
  Shifts <mark::> to the right.
  */
  right: -1.5ch;
  width: 3ch;
  padding: 1ex 1ch 1ex 0;
  border-top-right-radius: 10px;
  border-bottom-right-radius: 10px;
  /*
  Should be relatively smaller than the line-height of root because of 
  it's large padding. <mark::> is the end of the highlight.
  */
  line-height: 1.65ex;
  background-color: hsl(200deg 100% 80%);
}
<p>Here is some text that isn't highlighted and it wraps around.</p>

<div class='anchor'>Here is some text. Here is some highlighted text.
  <p class='ghost'>Here is some text. <mark>Here is some highlighted text.</mark></p>
</div>

<div class='anchor'>All of this is highlighted.
  <p class='ghost'><mark>All of this is highlighted.</mark></p>
</div>

<div class='anchor'>This text is highlighted. This text is not.
  <p class='ghost'><mark>This text is highlighted.</mark> This text is not.</p>
</div>

Simpler solution with relative positioning, that doesn't require duplicating the content. (I don't know if this will break though for certain situations though.)

The left padding is cancelled out by the negative left offset.

p {
  width: 12em;
}


mark.with-relative {
  border-radius: 10px;
  padding: 0.4em 0.2em 0.4em 0.25em;
  position: relative;
  z-index: -1;
  left: -0.25em;
  background-color: hsl(200deg 100% 80%);
  box-decoration-break: clone;
}
<p>Some text here.&ThinSpace;<mark class="with-relative">This text is also highlighted.</mark>But this text isn't.</p>

This adds a little bit of extra space after a highlight ends on a new line, but that's OK for my use case.

Related