1

I'm working on a text highlight component, the idea is for paragraph of text, you might highlight a word or a sentence and this in the page margin some comment corresponding to that highlight will be displayed.

enter image description here

I'm now wanting to add some hover functionality, where if you mouse over the highlight, both the highlight and the comment will be emphasised, and vice versa.

For regular css, hover styling can easily be achieved with the :hover selector.

Currently I'm going down the path of adding onMouseEnter and onMouseLeave listeners to each of the highlight and the comment, and then using that do add a '.hover' class that I'll target.

Is there a much easier way to do this?

What I'm thinking of is how elements can similarly be bound, for example how the for attribute will allow clicking a label in one place to focus on a different element.

3
  • It could be done in CSS using :has(), but you'd have to write a selector for every single highlight/comment combo. Or it could perhaps also be done by wrapping the highlight into an extra container, and then putting the comment into that container as well - and using anchor positioning to somehow get them to display in the desired location. (Browser support for anchor positioning isn't fully there yet though.)
    – C3roe
    Commented yesterday
  • 1
    The "path of adding onMouseEnter and onMouseLeave listeners" should be an easy and clean solution and can be implemented using vanilla javascript. If you already have some code which is not working you can share it and maybe we can help improve it. Commented yesterday
  • Important question here is whether DOM-wise these "notes" should be isolated from the main text and linked through some attributes, or intertwined with their targets physically following them (with SR-only skip links, presumably). (This pretty much resembles ongoing dilemma how the heck we should do footnotes and marginalia/side notes. (Aside: anybody around here remembers the HTML3 <fn> proposal?)
    – myf
    Commented yesterday

3 Answers 3

3

I will point out that using the for attribute does appear to work for this use case, in brave at least.

output {
 color: MarkText;
 background-color: Mark;
 &:hover {
  color: SelectedItemText;
  background-color: SelectedItem;
  outline: 3px solid SelectedItem;
 }
}
main {
 display: flex;
 & > * { padding: 1rem; border: 1px solid}
}
html { color-scheme: dark light;}
<main>
 <article>
  <p>Some <label for="tfn1"><output id="fn1">noted</output></label> text
  <p><label for="tfn2"><output id="fn2">Other</output></label> noted text
 </article>
 <aside>
  <p><label for="fn1"><output id="tfn1">"noted" label</output></label>
  <p><label for="fn2"><output id="tfn2">"Other" label</output></label>
 </aside>
</main>

But you'd need to consider how this is going to behave with screen readers etc. As mentioned in the comments, validator complains that:

Any output descendant of a label element with a for attribute must have an ID value that matches that for attribute.

2
  • 1
    this is invalid HTML
    – Bravo
    Commented yesterday
  • an <output> tag needs to be clsoed with </output> not with </div>
    – tacoshy
    Commented yesterday
2

Other (no-JS) solution could be simply leave marked text and its related note directly adjacent and group them in a common wrapper, visually positioning (or even floating) the note outside. This way the :hover of the common ancestor would be trivial

html {
 color-scheme: dark light;
 --margo: calc(min(40vw, 30rem));
}
.marginalia {
 p:has(&) {
  padding-right:  calc(var(--margo) + 2rem);
  position: relative;
 }
 /* The note. Just a dumb positioning. */
 & small {
  width: var(--margo);
  position: absolute;
  right: 0;
  font-size: 1rem;
  background-color: mark;
  color: marktext;
 }
 /* Hover on the common parent, efect on children */
 &:hover mark,
 &:hover small {
  background-color: SelectedItem;
  color: SelectedItemText;
  outline: 3px solid SelectedItem;
 }
 /* Think "SR only" */
 & span {
  position: absolute;
  transform: scale(0);
 }
}
<p>
 Some text with annotated
 <span class="marginalia">
  <mark>word</mark>
  <small>
   <span>(Note: [<a href#="endnote01">skip it</a>]:</span>
   Something about the word.
   <span>Note end.) <a name="endnote01"></a></span>
  </small>
 </span>
 followed by other text.
</p>
<p>
 And
 <span class="marginalia">
  <mark>other word</mark>
  <small>
   <span>(Note: [<a href#="endnote02">skip it</a>]:</span>
   Something about the other word.
   <span>Note end.) <a name="endnote02"></a></span>
  </small>
 </span>
 followed by some more text.
</p>

Added some aid for skipping the note for the screen readers, as well as crude visual hiding. The positioning is also only for demonstrative purposes: more notes on the same line would overlap (I think there is some modern CSS perk that could handle it in (near) future, but haven't looked for it yet.). Also this way marginalia could not hold block level elements, what might be a culprit.

1

Currently I'm going down the path of adding onMouseEnter and onMouseLeave listeners to each of the highlight and the comment, and then using that do add a .hover class that I'll target.

This can be easily delegated to couple listeners and some attribute contract, what does not look that much terrible in the end.

const markidAttr = 'data-markid';
document.documentElement.addEventListener(
  'mouseenter',
  function(event) {
    const t = event.target;
    if (!t.hasAttribute(markidAttr)) return
    const id = t.getAttribute(markidAttr);
    document.querySelectorAll(`[${markidAttr}="${id}"]`)
      .forEach(_ => _.classList.add('hover'))
  }, {
    capture: true
  }
);
document.documentElement.addEventListener(
  'mouseleave',
  function(event) {
    const t = event.target;
    if (!t.hasAttribute(markidAttr)) return
    const id = t.getAttribute(markidAttr);
    document.querySelectorAll(`[${markidAttr}="${id}"]`)
      .forEach(_ => _.classList.remove('hover'))
  }, {
    capture: true
  }
);
.hover {
  outline: 3px solid SelectedItem;
  background-color: SelectedItem;
  color: SelectedItemText;
}

main {
  display: flex;

  &>* {
    padding: 1rem;
    border: 1px solid
  }
}

html {
  color-scheme: dark light;
}
<main>
  <article>
    <p>Some <mark data-markid="1">noted</mark> text
    <p><mark data-markid="2">Other</mark> noted text
  </article>
  <aside>
    <p><mark data-markid="1">"noted" label</mark>
    <p><mark data-markid="2">"Other" label</mark>
  </aside>
</main>

This is nearly non-semantic; <mark data-markid="…"> is hardly accessible. Presumably some ARIA structures (aria-labelledby?) could be used here for better semantics and accessibility, but I don't feel confident about the best practice here.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.