If all you’ve gotten is a hammer, every part seems to be like a nail.
It’s simple to default to what . In the case of toggling content material, that is likely to be reaching for show: none or opacity: 0 with some JavaScript sprinkled in. However the internet is extra “trendy” right now, so maybe now could be the appropriate time to get a birds-eye view of the other ways to toggle content material — which native APIs are literally supported now, their execs and cons, and a few issues about them that you just may not know (resembling any pseudo-elements and different non-obvious stuff).
So, let’s spend a while taking a look at disclosures (<particulars> and <abstract>), the Dialog API, the Popover API, and extra. We’ll take a look at the appropriate time to make use of each relying in your wants. Modal or non-modal? JavaScript or pure HTML/CSS? Undecided? Don’t fear, we’ll go into all that.
Disclosures (<particulars> and <abstract>)
Use case: Accessibly summarizing content material whereas making the content material particulars togglable independently, or as an accordion.
Getting into launch order, disclosures — recognized by their components as <particulars> and <abstract> — marked the primary time we have been capable of toggle content material with out JavaScript or bizarre checkbox hacks. However lack of internet browser assist clearly holds new options again at first, and this one particularly got here with out keyboard accessibility. So I’d perceive for those who haven’t used it because it got here to Chrome 12 means again in 2011. Out of sight, out of thoughts, proper?
Right here’s the low-down:
It’s purposeful with out JavaScript (with none compromises).
It’s totally stylable with out look: none or the like.
You may disguise the marker with out non-standard pseudo-selectors.
You may join a number of disclosures to create an accordion.
Aaaand… it’s totally animatable, as of 2024.
Marking up disclosures
What you’re in search of is that this:
<particulars>
<abstract>Content material abstract (at all times seen)</abstract>
Content material (visibility is toggled when abstract is clicked on)
</particulars>
Behind the scenes, the content material’s wrapped in a pseudo-element that as of 2024 we are able to choose utilizing ::details-content. So as to add to this, there’s a ::marker pseudo-element that signifies whether or not the disclosure’s open or closed, which we are able to customise.
With that in thoughts, disclosures really appear to be this beneath the hood:
<particulars>
<abstract><::marker></::marker>Content material abstract (at all times seen)</abstract>
<::details-content>
Content material (visibility is toggled when abstract is clicked on)
</::details-content>
</particulars>
To have the disclosure open by default, give <particulars> the open attribute, which is what occurs behind the scenes when disclosures are opened anyway.
<particulars open> … </particulars>
Styling disclosures
Let’s be actual: you in all probability simply wish to lose that annoying marker. Effectively, you are able to do that by setting the show property of <abstract> to something however list-item:
abstract {
show: block; /* Or the rest that is not list-item */
}
Alternatively, you may modify the marker. The truth is, the instance under makes use of Font Superior to switch it with one other icon, however remember that ::marker doesn’t assist many properties. Probably the most versatile workaround is to wrap the content material of <abstract> in a component and choose it in CSS.
<particulars>
<abstract><span>Content material abstract</span></abstract>
Content material
</particulars>
particulars {
/* The marker */
abstract::marker {
content material: “f150”;
font-family: “Font Superior 6 Free”;
}
/* The marker when <particulars> is open */
&[open] abstract::marker {
content material: “f151”;
}
/* As a result of ::marker doesn’t assist many properties */
abstract span {
margin-left: 1ch;
show: inline-block;
}
}
Creating an accordion with a number of disclosures
To create an accordion, title a number of disclosures (they don’t even should be siblings) with a reputation attribute and an identical worth (much like the way you’d implement <enter sort=”radio”>):
<particulars title=”starWars” open>
<abstract>Prequels</abstract>
<ul>
<li>Episode I: The Phantom Menace</li>
<li>Episode II: Assault of the Clones</li>
<li>Episode III: Revenge of the Sith</li>
</ul>
</particulars>
<particulars title=”starWars”>
<abstract>Originals</abstract>
<ul>
<li>Episode IV: A New Hope</li>
<li>Episode V: The Empire Strikes Again</li>
<li>Episode VI: Return of the Jedi</li>
</ul>
</particulars>
<particulars title=”starWars”>
<abstract>Sequels</abstract>
<ul>
<li>Episode VII: The Power Awakens</li>
<li>Episode VIII: The Final Jedi</li>
<li>Episode IX: The Rise of Skywalker</li>
</ul>
</particulars>
Utilizing a wrapper, we are able to even flip these into horizontal tabs:
<div> <!– Flex wrapper –>
<particulars title=”starWars” open> … </particulars>
<particulars title=”starWars”> … </particulars>
<particulars title=”starWars”> … </particulars>
</div>
div {
hole: 1ch;
show: flex;
place: relative;
particulars {
min-height: 106px; /* Prevents content material shift */
&[open] abstract,
&[open]::details-content {
background: #eee;
}
&[open]::details-content {
left: 0;
place: absolute;
}
}
}
…or, utilizing 2024’s Anchor Positioning API, vertical tabs (identical HTML):
div {
show: inline-grid;
anchor-name: –wrapper;
particulars[open] {
abstract,
&::details-content {
background: #eee;
}
&::details-content {
place: absolute;
position-anchor: –wrapper;
high: anchor(high);
left: anchor(proper);
}
}
}
In case you’re in search of some wild concepts on what we are able to do with the Popover API in CSS, take a look at John Rhea’s article during which he makes an interactive recreation solely out of disclosures!
Including JavaScript performance
Wish to add some JavaScript performance?
// Optionally available: choose and loop a number of disclosures
doc.querySelectorAll(“particulars”).forEach(particulars => {
particulars.addEventListener(“toggle”, () => {
// The disclosure was toggled
if (particulars.open) {
// The disclosure was opened
} else {
// The disclosure was closed
}
});
});
Creating accessible disclosures
Disclosures are accessible so long as you comply with just a few guidelines. For instance, <abstract> is mainly a <label>, which means that its content material is introduced by display readers when in focus. If there isn’t a <abstract> or <abstract> isn’t a direct little one of <particulars> then the person agent will create a label for you that usually says “Particulars” each visually and in assistive tech. Older internet browsers would possibly insist that it’s the first little one, so it’s finest to make it so.
So as to add to this, <abstract> has the position of button, so no matter’s invalid inside a <button> can be invalid inside a <abstract>. This consists of headings, so you may fashion a <abstract> as a heading, however you may’t really insert a heading right into a <abstract>.
The Dialog component (<dialog>)
Use case: Modals
Now that we now have the Popover API for non-modal overlays, I believe it’s finest if we begin to consider dialogs as modals though the present() technique does enable for non-modal dialogs. The benefit that the popover attribute has over the <dialog> component is that you need to use it to create non-modal overlays with out JavaScript, so for my part there’s no profit to non-modal dialogs anymore, which do require JavaScript. For readability, a modal is an overlay that makes the primary doc inert, whereas with non-modal overlays the primary doc stays interactive. There are just a few different options that modal dialogs have out-of-the-box as effectively, together with:
a stylable backdrop,
an autofocus onto the primary focusable component throughout the <dialog> (or, as a backup, the <dialog> itself — embody an aria-label on this case),
a spotlight lure (because of the primary doc’s inertia),
the esc key closes the dialog, and
each the dialog and the backdrop are animatable.Marking up and activating dialogs
Begin with the <dialog> component:
<dialog> … </dialog>
It’s hidden by default and, much like <particulars>, we are able to have it open when the web page masses, though it isn’t modal on this state of affairs because it doesn’t comprise interactive content material as a result of it doesn’t opened with showModal().
<dialog open> … </dialog>
I can’t say that I’ve ever wanted this performance. As a substitute, you’ll probably wish to reveal the dialog upon some type of interplay, resembling the clicking of a button — so right here’s that button:
<button data-dialog=”dialogA”>Open dialogA</button>
Wait, why are we utilizing knowledge attributes? Effectively, as a result of we’d wish to hand over an identifier that tells the JavaScript which dialog to open, enabling us so as to add the dialog performance to all dialogs in a single snippet, like this:
// Choose and loop all components with that knowledge attribute
doc.querySelectorAll(“[data-dialog]”).forEach(button => {
// Pay attention for interplay (click on)
button.addEventListener(“click on”, () => {
// Choose the corresponding dialog
const dialog = doc.querySelector(`#${ button.dataset.dialog }`);
// Open dialog
dialog.showModal();
// Shut dialog
dialog.querySelector(“.closeDialog”).addEventListener(“click on”, () => dialog.shut());
});
});
Don’t overlook so as to add an identical id to the <dialog> so it’s related to the <button> that exhibits it:
<dialog id=”dialogA”> <!– id and data-dialog = dialogA –> … </dialog>
And, lastly, embody the “shut” button:
<dialog id=”dialogA”>
<button class=”closeDialog”>Shut dialogA</button>
</dialog>
Notice: <type technique=”dialog”> (that has a <button>) or <button formmethod=”dialog”> (wrapped in a <type>) additionally closes the dialog.
Find out how to stop scrolling when the dialog is open
Stop scrolling whereas the modal’s open, with one line of CSS:
physique:has(dialog:modal) { overflow: hidden; }
Styling the dialog’s backdrop
And eventually, we now have the backdrop to cut back distraction from what’s beneath the highest layer (this is applicable to modals solely). Its types could be overwritten, like this:
::backdrop {
background: hsl(0 0 0 / 90%);
backdrop-filter: blur(3px); /* A enjoyable property only for backdrops! */
}
On that be aware, the <dialog> itself comes with a border, a background, and a few padding, which you would possibly wish to reset. Really, popovers behave the identical means.
Coping with non-modal dialogs
To implement a non-modal dialog, use:
present() as a substitute of showModal()
dialog[open] (targets each) as a substitute of dialog:modal
Though, as I mentioned earlier than, the Popover API doesn’t require JavaScript, so for non-modal overlays I believe it’s finest to make use of that.
The Popover API (<component popover>)
Use case: Non-modal overlays
Popups, mainly. Appropriate use instances embody tooltips (or toggletips — it’s necessary to know the distinction), onboarding walkthroughs, notifications, togglable navigations, and different non-modal overlays the place you don’t wish to lose entry to the primary doc. Clearly these use instances are totally different to these of dialogs, however nonetheless popovers are extraordinarily superior. Functionally they’re identical to simply dialogs, however not modal and don’t require JavaScript.
Marking up popovers
To start, the popover wants an id in addition to the popover attribute with the guide worth (which implies clicking exterior of the popover doesn’t shut it), the auto worth (clicking exterior of the popover does shut it), or no worth (which implies the identical factor). To be semantic, the popover could be a <dialog>.
<dialog id=”tooltipA” popover> … </dialog>
Subsequent, add the popovertarget attribute to the <button> or <enter sort=”button”> that we wish to toggle the popover’s visibility, with a worth matching the popover’s id attribute (that is optionally available since clicking exterior of the popover will shut it anyway, until popover is ready to guide):
<dialog id=”tooltipA” popover>
<button popovertarget=”tooltipA”>Conceal tooltipA</button>
</dialog>
Place one other a type of buttons in your essential doc, so as to present the popover. That’s proper, popovertarget is definitely a toggle (until you specify in any other case with the popovertargetaction attribute that accepts present, disguise, or toggle as its worth — extra on that later).
Styling popovers
By default, popovers are centered throughout the high layer (like dialogs), however you in all probability don’t need them there as they’re not modals, in any case.
<essential>
<button popovertarget=”tooltipA”>Present tooltipA</button>
</essential>
<dialog id=”tooltipA” popover>
<button popovertarget=”tooltipA”>Conceal tooltipA</button>
</dialog>
You may simply pull them right into a nook utilizing mounted positioning, however for a tooltip-style popover you’d need it to be relative to the set off that opens it. CSS Anchor Positioning makes this tremendous simple:
essential [popovertarget] {
anchor-name: –trigger;
}
margin: 0;
position-anchor: –trigger;
high: calc(anchor(backside) + 10px);
justify-self: anchor-center;
}
/* This additionally works however isn’t wanted
until you’re utilizing the show property
[popover]:popover-open {
…
}
*/
The issue although is that you need to title all of those anchors, which is okay for a tabbed element however overkill for a web site with fairly just a few tooltips. Fortunately, we are able to match an id attribute on the button to an anchor attribute on the popover, which isn’t well-supported as of November 2024 however will do for this demo:
<essential>
<!– The id ought to match the anchor attribute –>
<button id=”anchorA” popovertarget=”tooltipA”>Present tooltipA</button>
<button id=”anchorB” popovertarget=”tooltipB”>Present tooltipB</button>
</essential>
<dialog anchor=”anchorA” id=”tooltipA” popover>
<button popovertarget=”tooltipA”>Conceal tooltipA</button>
</dialog>
<dialog anchor=”anchorB” id=”tooltipB” popover>
<button popovertarget=”tooltipB”>Conceal tooltipB</button>
</dialog>
essential [popovertarget] { anchor-name: –anchorA; } /* Now not wanted */
[popover] {margin: 0;
position-anchor: –anchorA; /* Now not wanted */
high: calc(anchor(backside) + 10px);
justify-self: anchor-center;
}
The subsequent challenge is that we count on tooltips to point out on hover and this doesn’t try this, which implies that we have to use JavaScript. Whereas this appears sophisticated contemplating that we are able to create tooltips far more simply utilizing ::earlier than/::after/content material:, popovers enable HTML content material (during which case our tooltips are literally toggletips by the best way) whereas content material: solely accepts textual content.
Including JavaScript performance
Which leads us to this…
Okay, so let’s check out what’s occurring right here. First, we’re utilizing anchor attributes to keep away from writing a CSS block for every anchor component. Popovers are very HTML-focused, so let’s use anchor positioning in the identical means. Secondly, we’re utilizing JavaScript to point out the popovers (showPopover()) on mouseover. And lastly, we’re utilizing JavaScript to cover the popovers (hidePopover()) on mouseout, however not in the event that they comprise a hyperlink as clearly we would like them to be clickable (on this state of affairs, we additionally don’t disguise the button that hides the popover).
<essential>
<button id=”anchorLink” popovertarget=”tooltipLink”>Open tooltipLink</button>
<button id=”anchorNoLink” popovertarget=”tooltipNoLink”>Open tooltipNoLink</button>
</essential>
<dialog anchor=”anchorLink” id=”tooltipLink” popover>Has <a href=”#”>a hyperlink</a>, so we are able to’t disguise it on mouseout
<button popovertarget=”tooltipLink”>Conceal tooltipLink manually</button>
</dialog>
<dialog anchor=”anchorNoLink” id=”tooltipNoLink” popover>Doesn’t have a hyperlink, so it’s positive to cover it on mouseout routinely
<button popovertarget=”tooltipNoLink”>Conceal tooltipNoLink</button>
</dialog>
margin: 0;
high: calc(anchor(backside) + 10px);
justify-self: anchor-center;
/* No hyperlink? No button wanted */
&:not(:has(a)) [popovertarget] {
show: none;
}
}
/* Choose and loop all popover triggers */
doc.querySelectorAll(“essential [popovertarget]”).forEach((popovertarget) => {
/* Choose the corresponding popover */
const popover = doc.querySelector(`#${popovertarget.getAttribute(“popovertarget”)}`);
/* Present popover on set off mouseover */
popovertarget.addEventListener(“mouseover”, () => {
popover.showPopover();
});
/* Conceal popover on set off mouseout, however not if it has a hyperlink */
if (popover.matches(“:not(:has(a))”)) {
popovertarget.addEventListener(“mouseout”, () => {
popover.hidePopover();
});
}
});
Implementing timed backdrops (and sequenced popovers)
At first, I used to be positive that popovers having backdrops was an oversight, the argument being that they shouldn’t obscure a focusable essential doc. However possibly it’s okay for a few seconds so long as we are able to resume what we have been doing with out being compelled to shut something? At the least, I believe this works effectively for a set of onboarding ideas:
<!– Re-showing ‘A’ rolls the onboarding again to that step –>
<button popovertarget=”onboardingTipA” popovertargetaction=”present”>Restart onboarding</button>
<!– Hiding ‘A’ additionally hides subsequent ideas so long as the popover attribute equates to auto –>
<button popovertarget=”onboardingTipA” popovertargetaction=”disguise”>Cancel onboarding</button>
<ul>
<li id=”toolA”>Software A</li>
<li id=”toolB”>Software B</li>
<li id=”toolC”>One other software, “C”</li>
<li id=”toolD”>One other software — let’s name this one “D”</li>
</ul>
<!– onboardingTipA’s button triggers onboardingTipB –>
<dialog anchor=”toolA” id=”onboardingTipA” popover>
onboardingTipA <button popovertarget=”onboardingTipB” popovertargetaction=”present”>Subsequent tip</button>
</dialog>
<!– onboardingTipB’s button triggers onboardingTipC –>
<dialog anchor=”toolB” id=”onboardingTipB” popover>
onboardingTipB <button popovertarget=”onboardingTipC” popovertargetaction=”present”>Subsequent tip</button>
</dialog>
<!– onboardingTipC’s button triggers onboardingTipD –>
<dialog anchor=”toolC” id=”onboardingTipC” popover>
onboardingTipC <button popovertarget=”onboardingTipD” popovertargetaction=”present”>Subsequent tip</button>
</dialog>
<!– onboardingTipD’s button hides onboardingTipA, which in-turn hides all ideas –>
<dialog anchor=”toolD” id=”onboardingTipD” popover>
onboardingTipD <button popovertarget=”onboardingTipA” popovertargetaction=”disguise”>End onboarding</button>
</dialog>
::backdrop {
animation: 2s fadeInOut;
}
margin: 0;
align-self: anchor-center;
left: calc(anchor(proper) + 10px);
}
/*
After customers have had a few
seconds to breathe, begin the onboarding
*/
setTimeout(() => {
doc.querySelector(“#onboardingTipA”).showPopover();
}, 2000);
Once more, let’s unpack. Firstly, setTimeout() exhibits the primary onboarding tip after two seconds. Secondly, a easy fade-in-fade-out background animation runs on the backdrop and all subsequent backdrops. The primary doc isn’t made inert and the backdrop doesn’t persist, so consideration is diverted to the onboarding ideas whereas not feeling invasive.
Thirdly, every popover has a button that triggers the subsequent onboarding tip, which triggers one other, and so forth, chaining them to create a totally HTML onboarding circulation. Usually, exhibiting a popover closes different popovers, however this doesn’t look like the case if it’s triggered from inside one other popover. Additionally, re-showing a visual popover rolls the onboarding again to that step, and, hiding a popover hides it and all subsequent popovers — though that solely seems to work when popover equates to auto. I don’t totally perceive it nevertheless it’s enabled me to create “restart onboarding” and “cancel onboarding” buttons.
With simply HTML. And you may cycle by the ideas utilizing esc and return.
Creating modal popovers
Hear me out. In case you just like the HTML-ness of popover however the semantic worth of <dialog>, this JavaScript one-liner could make the primary doc inert, due to this fact making your popovers modal:
doc.querySelectorAll(“dialog[popover]”).forEach(dialog => dialog.addEventListener(“toggle”, () => doc.physique.toggleAttribute(“inert”)));
Nevertheless, the popovers should come after the primary doc; in any other case they’ll additionally turn out to be inert. Personally, that is what I’m doing for modals anyway, as they aren’t part of the web page’s content material.
<physique>
<!– All of it will turn out to be inert –>
</physique>
<!– Due to this fact, the modals should come after –>
<dialog popover> … </dialog>
Aaaand… breathe
Yeah, that was so much. However…I believe it’s necessary to take a look at all of those APIs collectively now that they’re beginning to mature, so as to actually perceive what they will, can’t, ought to, and shouldn’t be used for. As a parting reward, I’ll depart you with a transition-enabled model of every API:
Popping dialog (with fading backdrop)
Sliding popover (hamburger nav, as a result of why not?)
The Totally different (and Fashionable) Methods to Toggle Content material initially printed on CSS-Tips, which is a part of the DigitalOcean household. It’s best to get the e-newsletter.
Subscribe to MarketingSolution.
Receive web development discounts & web design tutorials.
Now! Lets GROW Together!