5

How can I select the first of the highest h* elements present in a DOM?

Something like

(h1, h2, h3, h4, h5, h6):first-of-ordered-set

I.e. if a DOM tree has, in this order, h2, h3, h1, h2, h1, it would select the first h1;
and if the DOM has h3, h3, h2, h2, h4, it would select the first h2.
Let's assume h* elements are not nested.

I suspect CSS doesn't have that power, right?

Somethink potentially usable: https://css-tricks.com/extremely-handy-nth-child-recipes-sass-mixins/

Edit: Why I want it: A CMS system takes this "first top heading" as a title of the document (post, page, ...). But it leaves it in the page. And then it shows the title twice - once as the post title and once in the body. JavaScript is removed. The top h* level may differ.

Ondra Žižka
  • 43,948
  • 41
  • 217
  • 277
  • CSS has loops? I am looking for pure CSS solution. – Ondra Žižka Jul 27 '18 at 22:52
  • No way in pure CSS – j08691 Jul 27 '18 at 22:52
  • I have an idea but not sure it will cover all the cases ... – Temani Afif Jul 27 '18 at 22:52
  • There is no way in CSS, but what are you trying to accomplish? – Ibu Jul 27 '18 at 22:57
  • @Ibu, I added the motivation – Ondra Žižka Jul 27 '18 at 23:00
  • I take it there's no guarantee that the heading will be the first child of a particular element so you can't just use a first-child selector? If that is the case, it seems like it could be unsafe to hide it if, say, the author of the post didn't put a heading in until a section heading half way through the post. In that case, hiding that section heading might be confusing. What about hiding the auto-generated heading that the CMS is presumably inserting? – lemoncucumber Jul 27 '18 at 23:12
  • That's right. But it's then up to the author not to put the highest `h*` elsewhere than the top :) Hiding the top heading is not viable, because sometimes, the title comes from metadata which are not rendered. So it would have no heading at all. – Ondra Žižka Jul 27 '18 at 23:31

2 Answers2

1

I found something. CSS can't do it. Yet.

W3C is drafting new features:

.post-content:has(h1, h2, h3, h4, h5, h6)

Together with :not(), this will allow what I need:

.post-content:has(h1) h1:first-of-kind,
.post-content:not(:has(h1)) h2:first-of-kind,
.post-content:not(:has(h1,h2)) h3:first-of-kind,
...

There's a catch - the :not() currently can only have a single "simple selector". If it supported more, then it would be achievable even without :has.

Greetings to the future readers, I hope it worked out. For now, I am leaving this open, maybe someone will figure out with CSS 3.1.

Ondra Žižka
  • 43,948
  • 41
  • 217
  • 277
  • Important note: `:has()` is will be available in JavaScript. See [this (minor) discussion](https://discourse.wicg.io/t/implementation-can-we-push-browser-vendors-to-implement-has/2047) on the WICG's Discourse where I explicitly requested we pushed browsers to implement it. – jhpratt Jul 27 '18 at 23:51
1

The main issue is that we don't have previous selector in CSS. For example, we can get the first h2 but if later we find a h1 we cannot have a selector to go backwards.

Here is the best you can do with CSS. You can hide all the elements after the needed one (so the element you want is the last visible one) but you cannot do the same with the previous elements.

h1:first-of-type,
h2:first-of-type,
h3:first-of-type,
h4:first-of-type {
  color:red;
}


h1:first-of-type ~ * {
  display:none;
}
h2:first-of-type ~ *:not(h1) {
  display:none;
}
h3:first-of-type ~ h4 {
  display:none;
}


.container {
 border:1px solid;
}
<div class="container">
  <h3>text 3</h3>
  <h1>text 1*</h1>
  <h2>text 2</h2>
  <h1>text 1</h1>
</div>
<div class="container">
  <h3>text 3</h3>
  <h2>text 2*</h2>
  <h2>text 2</h2>
  <h4>text 1</h4>
</div>
<div class="container">
  <h3>text 3</h3>
  <h3>text 2</h3>
  <h2>text 2*</h2>
  <h4>text 1</h4>
</div>
<div class="container">
  <h1>text 3*</h1>
  <h3>text 2</h3>
  <h2>text 2</h2>
  <h4>text 1</h4>
</div>

You may then combine with a small JS code to keep only the needed element:

$('h3:first-of-type').prevAll('h4').hide();
$('h2:first-of-type').prevAll('*:not(h1)').hide();
$('h1:first-of-type').prevAll('*').hide();
h1:first-of-type,
h2:first-of-type,
h3:first-of-type,
h4:first-of-type {
  color:red;
}


h1:first-of-type ~ * {
  display:none;
}
h2:first-of-type ~ *:not(h1) {
  display:none;
}
h3:first-of-type ~ h4 {
  display:none;
}

.container {
 border:1px solid;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="container">
  <h3>text 3</h3>
  <h1>text 1*</h1>
  <h2>text 2</h2>
  <h1>text 1</h1>
</div>
<div class="container">
  <h3>text 3</h3>
  <h2>text 2*</h2>
  <h2>text 2</h2>
  <h4>text 1</h4>
</div>
<div class="container">
  <h3>text 3</h3>
  <h3>text 2</h3>
  <h2>text 2*</h2>
  <h4>text 1</h4>
</div>
<div class="container">
  <h1>text 1*</h1>
  <h3>text 2</h3>
  <h2>text 2</h2>
  <h4>text 1</h4>
</div>

I used to make the element hidden but you can do the same with other styles:

$('h3:first-of-type').prevAll('h4').addClass('initial');
$('h2:first-of-type').prevAll('*:not(h1)').addClass('initial');
$('h1:first-of-type').prevAll('*').addClass('initial');
h1:first-of-type,
h2:first-of-type,
h3:first-of-type,
h4:first-of-type {
  color:red;
  font-family:cursive;
  font-style:italic;
}


h1:first-of-type ~ *,
h2:first-of-type ~ *:not(h1),
h3:first-of-type ~ h4,
h1.initial,h2.initial,h3.initial,h4.initial{
  color:initial;
  font-family:initial;
  font-style:initial;
}

.container {
 border:1px solid;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="container">
  <h3>text 3</h3>
  <h1>text 1*</h1>
  <h2>text 2</h2>
  <h1>text 1</h1>
</div>
<div class="container">
  <h3>text 3</h3>
  <h2>text 2*</h2>
  <h2>text 2</h2>
  <h4>text 1</h4>
</div>
<div class="container">
  <h3>text 3</h3>
  <h3>text 2</h3>
  <h2>text 2*</h2>
  <h4>text 1</h4>
</div>
<div class="container">
  <h1>text 1*</h1>
  <h3>text 2</h3>
  <h2>text 2</h2>
  <h4>text 1</h4>
</div>
Temani Afif
  • 245,468
  • 26
  • 309
  • 415
  • 1
    Right, CSS only goes forward, and I believe that's something to do with performance. Probably CSS only traverses the tree once, with optimizations like indexes for element names and class names and ids. I believe that's also the reason for the controversion around `has()`. – Ondra Žižka Jul 27 '18 at 23:55