I’m tentatively putting together the outline for a book on HTML + CSS. Why is an ontologist working on a CSS book? The question that may be more appropriate is why this ontologist is not writing a book on SPARQL and SHACL. That’s in the works, too, but I’ve always had a soft spot for CSS. One associate told me adamantly that an Information Architect was a glorified UI/UX designer while a Data Architect was a true ontologist. I think it’s a quibble. If you do not know how to present information, I don’t care how glorified the ontology is. You are failing in your duty as an architect, and quite frankly, CSS goes a long way toward making such presentations meaningful.
Some very intriguing things are taking place in CSS lately, and if you’ve been away from the language, it’s worth taking a few afternoons and re-immersing yourself. CSS variables, calc(), an increasingly sophisticated text-to-speech layer, and so forth. For my money, though, the has()
operator is easily one of the most useful changes (flex comes second, but it’s been around for a while now).
Many years ago, when I was an invited expert on the SVG specification, I made a recommendation (when we were discussing SVG-based components) that we incorporate XPath to specify resources on a web page. The idea was well received in some quarters and absolutely ridiculed by others because XPath “wasn’t” CSS. I am still of the opinion that as a selector language, XPath was more expressive than CSS Selectors; however, that particular ship sailed decades ago now.
The reason why XPath is so powerful is that you can have expressions such as
/path/to/base/node[conditional expression containing other paths]/path/to/final/node
where the “predicate” (the part in square brackets) made it possible to specify conditional paths to prune the tree between root and target nodes.
That option has been missing from CSS since its inception in 1995, and it has often made for very unwieldy CSS that ended up with a great deal of dependency on Javascript. The DOM command querySelector() and querySelectorAll() helped things somewhat, but it’s only been in the last couple of years that this problem has been (more or less) solved for CSS, due to the has()
function.
This particular problem was brought home to me when trying to put together a tab selector using only CSS. Tabs are such an integral part of most web pages that you don’t think about them (and they come in handy when dealing with semantic applications). Still, while there are several tab components out there, they often require a significant memory footprint and are overkill for a lot of basic applications.
The problem is that tab interfaces generally split the tabs themselves from the content area that the tabs display. Prior to 2020, it wasn’t possible to handle this kind of separation because the CSS couldn’t readily interact between two distinct areas. With flex and grid both available, this limitation readily proved frustrating. You could do tabs with a relatively minimal amount of Javascript, but as a design principle, I prefer going to Javascript only if I cannot get HTML (or SVG) + CSS to do things on its own.
The has()
function solves that. For instance, consider the following HTML layout:
<div class="tabset">
<div class="tabbar">
<input type="radio" id="tab1" name="tabs" checked/>
<label for="tab1">Tab 1</label>
<input type="radio" id="tab2" name="tabs"/>
<label for="tab2">Tab 2</label>
<input type="radio" id="tab3" name="tabs"/>
<label for="tab3">Tab 3</label>
<input type="radio" id="tab4" name="tabs"/>
<label for="tab4">Tab 4</label>
<input type="radio" id="tab5" name="tabs"/>
<label for="tab5">Tab 5</label>
</div>
<div class="tabbox">
<div class="tab content1">
<h1>Section 1</h1>
<p>Section 1 Content
</div>
<div class="tab content2">
<h1>Section 2</h1>
<p>Section 2 Content
</div>
<div class="tab content3">
<h1>Section 3</h1>
<p>Section 3 Content
</div>
<div class="tab content4">
<h1>Section 4</h1>
<p>Section 4 Content
</div>
<div class="tab content5">
<h1>Section 5</h1>
<p>Section 5 Content
</div>
</div>
</div>
In this case, you have a general tab set that holds both the tab bar and the associated display box for that tab. In the flat space available via the ~ operator in CSS, this breakdown would simply not be feasible. However, with CSS, you can use the has() function to start with a node closer to the body node (rootward), then use this to filter only the node that met the necessary condition, as shown in Listing 2.
input { display: none; } /* hide radio buttons */
input + label { display: inline-block;
cursor:pointer;} /* show labels in line */
.tab { display: none } /* hide contents */
/* show contents only for selected tab */
.tabset:has(#tab1:checked) .tab.content1,
.tabset:has(#tab2:checked) .tab.content2,
.tabset:has(#tab3:checked) .tab.content3,
.tabset:has(#tab4:checked) .tab.content4,
.tabset:has(#tab5:checked) .tab.content5 { display: block; }
input + label { /* box with rounded corner */
border: 1px solid #999;
background: #EEE;
padding: 4px 12px;
border-radius: 4px 4px 0 0;
position: relative;
top: 1px;
}
input:checked + label { /* white background for selected tab */
background: #FFF;
border-bottom: 1px solid transparent;
}
.tabset.has(#tab5:checked).tab {
border-top: 1px solid #999;
padding: 12px;
}
.tabbox {
border:solid 1px;
width:400px;
height:max(50%,252px);
padding:10px;
}
s
The workhorse here is the has() function, especially in this block:
input { display: none; } /* hide radio buttons */
input + label { display: inline-block;
cursor:pointer;} /* show labels in line */
.tab { display: none } /* hide contents */
/* show contents only for selected tab */
.tabset:has(#tab1:checked) .tab.content1,
.tabset:has(#tab2:checked) .tab.content2,
.tabset:has(#tab3:checked) .tab.content3,
.tabset:has(#tab4:checked) .tab.content4,
.tabset:has(#tab5:checked) .tab.content5 { display: block; }
This identifies the node with the .tabset class (and, as such, encompasses both the tab bar and the tab box). The CSS then looked to see if any tabset has an identifier with a checked radio button. If that condition is satisfied, the CSS selector then retrieves the relevant tab box entry and displays it, as shown in the following example:
Section 1
Section 1 Content
Section 2
Section 2 Content
Section 3
Section 3 Content
Section 4
Section 4 Content
Section 5
Section 5 Content
There is a corresponding not() operator which provides a conditional test and retrieves those nodes that fail to satisfy the condition, and not() and has() can be wrapped in one another, e.g.,
.tabset:has(not(#tab1:checked)) .tab.content1
Conclusion
I’ll be publishing periodic sections from the HTML + CSS book as I get them done (these are generally cookbook-like tips and will likely be shorter than my usual ponderous tomes). My book Context will pull from many of the essays I’ve written here as well, as I’m exploring the rise of contextual computing and context-free grammars from BNF to XML to RDF to LLMs and beyond.
Kurt Cagle is the Editor in Chief of The Cagle Report, a former community editor for Data Science Central, and is the principal for Semantical LLC, as well as a regular contributing writer for Linked In. He has written twenty four books and hundreds of articleson programming and data interchange standards. He maintains a Calendly free consultation site at https://calendly.com/semantical – if you have a question, want to suggest a story, or just want to chat, set up a free consultation appointment with him there.
You must log in to post a comment.