A recent survey by Santa Cruz Software reveals that artificial intelligence (AI) is significantly transforming the design sector.
Among over 400 U.S.-based graphic designers surveyed, 98% report AI has altered their workflows, and 91% note a positive return on investment (ROI) from adopting AI tools. Creative directors (72%) view AI as a collaborative assistant that enhances creativity without replacing human input.
To stay competitive, 93% of designers have pursued AI training. Designers commonly use AI tools for image and video generation (67%), content creation (51%), and AI-powered design software (45%), showcasing AI’s role in boosting efficiency and innovation.
However, ethical concerns remain. While 65% view AI as ethical, 24% disagree, and concerns about copyright (55%), originality (46%), bias (34%), and privacy (41%) persist. Most designers (81%) are restricted to employer-approved AI tools, reflecting companies’ recognition of the need for ethical standards.
Designers favor AI for specific tasks like generating ideas (74%), client communications (51%), and repetitive tasks (43%), but 68% prefer human oversight for final design decisions.
Preferences for human vs. AI-generated designs are evenly split (46% each), with junior designers more inclined toward AI. Regarding attribution, 59% are comfortable taking credit for AI-assisted designs, though 38% express discomfort.
Andy is more than a great writer — he’s a teacher, too. And you’ll get to see that in spades next week when his brand-new course Andy is more than a great writer — he’s a teacher, too. And you’ll see that in spades next week when his brand-new course Complete CSS is released one week from today on November 26.
As someone who also runs a front-end course, I can tell you it takes a non-trivial amount of time and effort to put something like Complete CSS together. I’ve been able to sneak peek at the course and like love how it’s made for many CSS-Tricks readers — you know CSS and use it regularly but need to ratchet it up from good to great. If my course is for those just getting into CSS, Andy will graduate you from hobbyist to practitioner in Complete CSS. It’s the perfect next step for narrowing the ever-growing learning gaps in this industry.
Early bird price is £189 (~$240) which is a steep cut from the full £249 (~$325) price tag.
Yet, despite its widespread adoption, modern users are increasingly frustrated. Why? Because the once-revolutionary approach is failing to keep up with the evolving demands of today’s digital landscape.
In this article, we’ll dive into the limitations of responsive design, explore why it often falls short for modern users, and discuss how developers and designers can adapt to address these challenges.
The Promise of Responsive Design
When Ethan Marcotte introduced the concept of responsive web design (RWD) in 2010, it was groundbreaking. The methodology relied on flexible grids, fluid images, and media queries to ensure that a website could gracefully adapt to different screen sizes.
At its core, responsive design sought to enhance usability across devices, eliminate the need for maintaining separate mobile and desktop websites, and future-proof websites against new screen resolutions and device types. The approach was a perfect solution for its time, addressing the fragmented device market and providing a unified codebase for developers. However, as technology and user behavior evolved, cracks began to show.
Where Responsive Design Falls Short
Device Diversity is Outpacing RWD Frameworks
In 2010, developers primarily worried about three types of screens: desktop, tablet, and mobile. Today, the landscape is far more complex. Foldable phones with dual screens, smartwatches with minuscule displays, large-format monitors and ultrawide screens, and smart TVs with built-in browsers have all entered the market.
Responsive design frameworks often rely on breakpoints—predefined screen widths where the layout shifts. But how do you define breakpoints for devices with vastly unique form factors? This rigidity makes many responsive websites feel awkward or unoptimized on newer devices.
Performance Issues
Responsive design is frequently implemented with the philosophy of “designing for the smallest screen first and scaling up.” While this approach (mobile-first design) has its merits, it can lead to performance issues on larger devices.
Many websites load unnecessary assets, scripts, and styles meant for high-resolution screens, even when accessed on mobile devices. The result is websites that are bloated and slow, particularly in regions with limited internet bandwidth.
Modern users demand speed, and when responsive sites fail to deliver, frustration sets in.
One Size Does Not Fit All
Responsive design’s primary goal is to create a consistent experience across devices, but consistency can sometimes come at the cost of usability.
Navigation menus designed for desktop often become cumbersome on mobile. Call-to-action buttons that look great on a phone may appear oversized or intrusive on a tablet. Typography scaling can render text too small to read comfortably on certain devices.
Instead of creating truly optimized experiences for each device, responsive design tends to settle for mediocrity across all platforms.
Lack of Context Awareness
Modern users’ needs extend beyond screen size. They’re influenced by device capabilities (e.g., touchscreens vs. keyboard and mouse), connection speeds, and bandwidth limits, as well as user intentions (e.g., quick information lookup vs. in-depth reading).
Responsive design doesn’t account for these nuances. For instance, a user on a mobile device might prefer simplified content or fewer images to save data, but a responsive site often delivers the same content as its desktop counterpart.
Accessibility Challenges
Accessibility is a crucial part of modern web design, yet many responsive websites fail to meet basic accessibility standards. Issues such as poorly scaling elements that break screen reader compatibility, over-reliance on hover states for navigation, and improper contrast ratios that become unreadable on certain screens are common.
While these problems aren’t exclusive to responsive design, they’re exacerbated by the “one-size-fits-all” mentality that responsive frameworks encourage.
The Rise of New User Expectations
Modern users are not just browsing the web; they’re living digitally integrated lives. Their expectations have evolved, shaped by experiences with intuitive apps, fast-loading progressive web applications (PWAs), and AI-driven interfaces.
Users now expect personalized experiences that cater to their preferences, location, and behavior. Responsive design, by focusing solely on screen size, doesn’t inherently support personalization. A generic layout fails to address users’ individual needs.
Websites are no longer static pages; they’re dynamic, interactive experiences. Parallax scrolling, 3D animations, and complex interactions often break or degrade on responsive sites because these elements weren’t designed with device-specific capabilities in mind.
Additionally, the rise of voice assistants and gesture-based navigation (e.g., swiping on phones) means users interact with devices in ways that traditional responsive design doesn’t consider.
Moving Beyond Responsive Design
To meet the demands of modern users, web design must evolve beyond the limitations of traditional responsive frameworks. Adaptive design offers a way forward by creating multiple fixed layouts tailored to specific device types. Instead of relying on fluid grids, adaptive sites detect the user’s device and serve the most appropriate version.
This approach offers more control over the user experience on each device and reduces unnecessary elements, optimizing performance. However, it requires more design and development effort and can increase maintenance complexity.
Progressive Web Apps (PWAs) blur the lines between websites and native apps by offering offline functionality, lightning-fast performance, and a more app-like experience. By focusing on the capabilities of the device rather than its screen size, PWAs can deliver a more tailored experience.
Another approach involves prioritizing content. Instead of cramming all desktop content into a smaller screen, designers should focus on delivering only the most relevant information based on context. Techniques such as progressive disclosure, which shows essential content first and hides secondary elements behind expandable sections, and conditional loading, which serves lightweight versions of content for users on slower connections, can enhance usability.
Context-aware design, which uses data like location, device type, and user behavior to adapt the experience dynamically, is another promising direction. For example, displaying a map when a user accesses a site near a physical store or offering larger touch targets for devices without a keyboard or mouse can greatly improve the user experience.
Accessibility-first design ensures that all users, regardless of their abilities or devices, can navigate and interact with the site effectively. This includes testing designs with screen readers and keyboard navigation as well as implementing scalable typography and flexible layouts.
Finally, component-based design systems like React or Vue allow developers to create reusable, modular elements that adapt dynamically. Components can respond to both device size and capabilities and load selectively to optimize performance.
A Call to Action for Designers and Developers
It’s clear that the future of web design requires more than just “making things fit.” Designers and developers must embrace a mindset of adaptability rather than rigid frameworks. They must prioritize user-first thinking that considers context, behavior, and preferences, while leveraging new tools and technologies to address the limitations of responsive design.
Responsive design isn’t dead, but it’s no longer enough. By acknowledging its shortcomings and building on its foundations, we can create experiences that truly resonate with modern users.
Responsive design has served us well, but the digital world has changed. It’s time for web professionals to look forward and design for the future, not just for the screen.
You’ve played Flexbox Froggy before, right? Or maybe Grid Garden? They’re both absolute musts for learning the basics of modern CSS layout using Flexbox and CSS Grid. I use both games in all of the classes I teach and I never get anything but high-fives from my students because they love them so much.
Thomas wrote in to share his latest game with us: Anchoreum.
I’ll bet the two nickels in my pocket that you know this game’s all about CSS Anchor Positioning. I love that Thomas has jumped on this so quickly because the feature is still fresh, and indeed is currently only supported in a couple of browsers at the moment.
This is the perfect time to learn about anchor positioning. It’s still relatively early days, but things are baked enough to be supported in Chrome and Edge so you can access the games. If you haven’t seen Juan’s big ol’ guide on anchor positioning, that’s another dandy way to get up to speed.
The objective is less on-the-nose than Flexbox Froggy and Grid Garden, which both lean heavily into positioning elements to complete game tasks. For example, Flexbox Froggy is about positioning frogs safely on lilypads. Grid Garden wants you to water specific garden areas to feed your carrots. Anchoreum? You’re in a museum and need to anchor labels to museum artifacts. I know, attaching target elements to the same anchor over and again could get boring. But thankfully the game goes beyond simple positioning by getting into multiple anchors, spanning, and position fallbacks.
Whatever the objective, the repetition is good for developing muscle memory and the overall outcome is still the same: learn CSS Anchor Positioning. I’m already planning how and where I’m going to use Anchoreum in my curriculum. It’s not often we get a fun interactive learning resource like this for such a new web feature and I think it’s worth jumping on it sooner rather than later.
Thomas prepped a video trailer for the game so I thought I’d drop that for reference.
Another title from A Book Apart has been re-released for free. The latest? Tim Brown’s Flexible Typesetting. I may not be the utmost expert on typography and its best practices but I do remember reading this book (it’s still on the shelf next to me!) thinking maybe, just maybe, I might be able to hold a conversation about it with Robin when I finished it.
I still think I’m in “maybe” territory but that’s not Tim’s fault — I found the book super helpful and approachable for noobs like me who want to up our game. For the sake of it, I’ll drop the chapter titles here to give you an idea of what you’ll get.
What is typsetting?
Preparing text and code (planning is definitely part of the typesetting process)
Selecting typefaces (this one helped me a lot!)
Shaping text blocks (modern CSS can help here)
Crafting compositions (great if you’re designing for long-form content)
Fear not, for this guide will arm you with the tools and humor needed to navigate these meetings with style (and sanity) intact.
1. Master Your Facial Expressions
First things first: when the client says, “Can you make it pop?”, your face is a canvas that needs to remain as neutral as Helvetica.
Resist the urge to roll your eyes so hard they’re at risk of orbiting the sun. Instead, master the subtle nod paired with a thoughtful “Hmm,” as if you’re a wise sage deciphering the mysteries of the universe.
Pro tip: Practice in front of the mirror so your face says, “I’m intrigued,” instead of “I’m Googling how to change careers.”
2. Decode the Pop Factor
What does “pop” mean? Good question! No one knows. In fact, ‘pop’ is a cryptid, somewhere between Bigfoot and the Loch Ness Monster in terms of actual sightings.
But that won’t stop clients from using it like it’s on page one of the design manual.
Here are a few common translations of ‘pop’:
Make it brighter: Double-check that your design isn’t already capable of blinding astronauts. If not, unleash a palette of neon.
Add more contrast: Prepare to juggle gradients and shadows like a circus act.
Something ‘fun’: Add confetti! No one can complain about confetti.
3. The Color Gauntlet
When a client asks for “more pop,” 99% of the time they want color. Lots of it. The kind of color that makes you question your entire aesthetic sensibility. You might have envisioned a clean, minimalist layout—something that whispers sophistication.
But now you need to bring in more saturated hues than a ’80s fashion show.
Your strategy: Open your design software, set your color wheel to “Unleash Chaos,” and make peace with the fact that subtlety is no longer in the building.
4. Add Unnecessary Sparkle
If things start to go south and the client still looks unconvinced, here’s a designer secret: add a glow, shimmer, or ‘futuristic’ shine.
The magic of unnecessary sparkles is that clients often don’t know what they want until they see something that distracts them long enough to forget their original request.
Disclaimer: Excessive sparkles may lead to existential questioning of your career choices.
5. The “Just One More Thing” Loop
Once you’ve ‘popped’ the design to within an inch of its life, the client will inevitably say, “It’s great, but can you add just one more thing?” This is a pivotal moment where you must embrace your inner zen master.
Mental exercise: Picture yourself as a superhero whose only power is to smile and say, “Of course!” while internally questioning how many ‘one more things’ it takes to reach creative enlightenment.
6. The Great Font Debate
If your design involves text, prepare for the client to request that the headline “pop” more. Suddenly, every typeface you suggest will be compared to a distant cousin’s wedding invite or an obscure 1992 music festival poster. And just when you think you’ve found the perfect font, they’ll say, “Can we try Comic Sans?”
Response: Take a deep breath, channel your best customer service voice, and gently guide them away from the abyss.
7. When All Else Fails, Add an Exclamation Point!
Nothing says “pop” quite like adding an exclamation point to everything. Your client might not know why it works, but they’ll be convinced it does.
It’s the design equivalent of yelling, “Ta-da!” after revealing a trick.
Conclusion
At the end of the day, remember that “popping” is an art form, a mystical dance between client expectations and your creative limits.
Sometimes, it means finding that perfect middle ground between tasteful and tacky. Other times, it means letting go and accepting that your carefully crafted layout now resembles a carnival flyer.
Smile, nod, and don’t forget—you’re not just a designer. You’re a magician, conjuring bursts of color, sparkle, and fonts into creations that ‘pop’ just enough to keep everyone happy—or at least not asking for Comic Sans again.
It’s easy to default to what you know. When it comes to toggling content, that might be reaching for display: none or opacity: 0 with some JavaScript sprinkled in. But the web is more “modern” today, so perhaps now is the right time to get a birds-eye view of the different ways to toggle content — which native APIs are actually supported now, their pros and cons, and some things about them that you might not know (such as any pseudo-elements and other non-obvious stuff).
So, let’s spend some time looking at disclosures (
and ), the Dialog API, the Popover API, and more. We’ll look at the right time to use each one depending on your needs. Modal or non-modal? JavaScript or pure HTML/CSS? Not sure? Don’t worry, we’ll go into all that.
Disclosures ( and )
Use case: Accessibly summarizing content while making the content details togglable independently, or as an accordion.
CodePen Embed Fallback
Going in release order, disclosures — known by their elements as
and — marked the first time we were able to toggle content without JavaScript or weird checkbox hacks. But lack of web browser support obviously holds new features back at first, and this one in particular came without keyboard accessibility. So I’d understand if you haven’t used it since it came to Chrome 12 way back in 2011. Out of sight, out of mind, right?
Here’s the low-down:
It’s functional without JavaScript (without any compromises).
It’s fully stylable without appearance: none or the like.
You can hide the marker without non-standard pseudo-selectors.
You can connect multiple disclosures to create an accordion.
Aaaand… it’s fully animatable, as of 2024.
Marking up disclosures
What you’re looking for is this:
<details>
<summary>Content summary (always visible)</summary>
Content (visibility is toggled when summary is clicked on)
</details>
Behind the scenes, the content’s wrapped in a pseudo-element that as of 2024 we can select using ::details-content. To add to this, there’s a ::marker pseudo-element that indicates whether the disclosure’s open or closed, which we can customize.
With that in mind, disclosures actually look like this under the hood:
<details>
<summary><::marker></::marker>Content summary (always visible)</summary>
<::details-content>
Content (visibility is toggled when summary is clicked on)
</::details-content>
</details>
To have the disclosure open by default, give
the open attribute, which is what happens behind the scenes when disclosures are opened anyway.
<details open> ... </details>
Styling disclosures
Let’s be real: you probably just want to lose that annoying marker. Well, you can do that by setting the display property of
to anything but list-item:
summary {
display: block; /* Or anything else that isn't list-item */
}
CodePen Embed Fallback
Alternatively, you can modify the marker. In fact, the example below utilizes Font Awesome to replace it with another icon, but keep in mind that ::marker doesn’t support many properties. The most flexible workaround is to wrap the content of
details {
/* The marker */
summary::marker {
content: "f150";
font-family: "Font Awesome 6 Free";
}
/* The marker when <details> is open */
&[open] summary::marker {
content: "f151";
}
/* Because ::marker doesn’t support many properties */
summary span {
margin-left: 1ch;
display: inline-block;
}
}
CodePen Embed Fallback
Creating an accordion with multiple disclosures
CodePen Embed Fallback
To create an accordion, name multiple disclosures (they don’t even have to be siblings) with a name attribute and a matching value (similar to how you’d implement ):
<details name="starWars" open>
<summary>Prequels</summary>
<ul>
<li>Episode I: The Phantom Menace</li>
<li>Episode II: Attack of the Clones</li>
<li>Episode III: Revenge of the Sith</li>
</ul>
</details>
<details name="starWars">
<summary>Originals</summary>
<ul>
<li>Episode IV: A New Hope</li>
<li>Episode V: The Empire Strikes Back</li>
<li>Episode VI: Return of the Jedi</li>
</ul>
</details>
<details name="starWars">
<summary>Sequels</summary>
<ul>
<li>Episode VII: The Force Awakens</li>
<li>Episode VIII: The Last Jedi</li>
<li>Episode IX: The Rise of Skywalker</li>
</ul>
</details>
Using a wrapper, we can even turn these into horizontal tabs:
If you’re looking for some wild ideas on what we can do with the Popover API in CSS, check out John Rhea’s article in which he makes an interactive game solely out of disclosures!
Adding JavaScript functionality
Want to add some JavaScript functionality?
// Optional: select and loop multiple disclosures
document.querySelectorAll("details").forEach(details => {
details.addEventListener("toggle", () => {
// The disclosure was toggled
if (details.open) {
// The disclosure was opened
} else {
// The disclosure was closed
}
});
});
Creating accessible disclosures
Disclosures are accessible as long as you follow a few rules. For example,
is basically a , meaning that its content is announced by screen readers when in focus. If there isn’t a or isn’t a direct child of then the user agent will create a label for you that normally says “Details” both visually and in assistive tech. Older web browsers might insist that it be the first child, so it’s best to make it so.
To add to this,
has the role of button, so whatever’s invalid inside a is also invalid inside a . This includes headings, so you can style a as a heading, but you can’t actually insert a heading into a .
The Dialog element ()
Use case: Modals
CodePen Embed Fallback
Now that we have the Popover API for non-modal overlays, I think it’s best if we start to think of dialogs as modals even though the show() method does allow for non-modal dialogs. The advantage that the popover attribute has over the element is that you can use it to create non-modal overlays without JavaScript, so in my opinion there’s no benefit to non-modal dialogs anymore, which do require JavaScript. For clarity, a modal is an overlay that makes the main document inert, whereas with non-modal overlays the main document remains interactive. There are a few other features that modal dialogs have out-of-the-box as well, including:
a stylable backdrop,
an autofocus onto the first focusable element within the (or, as a backup, the itself — include an aria-label in this case),
a focus trap (as a result of the main document’s inertia),
the esc key closes the dialog, and
both the dialog and the backdrop are animatable.Marking up and activating dialogs
Start with the element:
<dialog> ... </dialog>
It’s hidden by default and, similar to
, we can have it open when the page loads, although it isn’t modal in this scenario since it does not contain interactive content because it doesn’t opened with showModal().
<dialog open> ... </dialog>
I can’t say that I’ve ever needed this functionality. Instead, you’ll likely want to reveal the dialog upon some kind of interaction, such as the click of a button — so here’s that button:
Wait, why are we using data attributes? Well, because we might want to hand over an identifier that tells the JavaScript which dialog to open, enabling us to add the dialog functionality to all dialogs in one snippet, like this:
// Select and loop all elements with that data attribute
document.querySelectorAll("[data-dialog]").forEach(button => {
// Listen for interaction (click)
button.addEventListener("click", () => {
// Select the corresponding dialog
const dialog = document.querySelector(`#${ button.dataset.dialog }`);
// Open dialog
dialog.showModal();
// Close dialog
dialog.querySelector(".closeDialog").addEventListener("click", () => dialog.close());
});
});
Don’t forget to add a matching id to the so it’s associated with the that shows it:
<dialog id="dialogA"> <!-- id and data-dialog = dialogA --> ... </dialog>
Note: (that has a ) or (wrapped in a ) also closes the dialog.
How to prevent scrolling when the dialog is open
Prevent scrolling while the modal’s open, with one line of CSS:
body:has(dialog:modal) { overflow: hidden; }
Styling the dialog’s backdrop
And finally, we have the backdrop to reduce distraction from what’s underneath the top layer (this applies to modals only). Its styles can be overwritten, like this:
::backdrop {
background: hsl(0 0 0 / 90%);
backdrop-filter: blur(3px); /* A fun property just for backdrops! */
}
On that note, the itself comes with a border, a background, and some padding, which you might want to reset. Actually, popovers behave the same way.
Dealing with non-modal dialogs
To implement a non-modal dialog, use:
show() instead of showModal()
dialog[open] (targets both) instead of dialog:modal
Although, as I said before, the Popover API doesn’t require JavaScript, so for non-modal overlays I think it’s best to use that.
The Popover API ()
Use case: Non-modal overlays
CodePen Embed Fallback
Popups, basically. Suitable use cases include tooltips (or toggletips — it’s important to know the difference), onboarding walkthroughs, notifications, togglable navigations, and other non-modal overlays where you don’t want to lose access to the main document. Obviously these use cases are different to those of dialogs, but nonetheless popovers are extremely awesome. Functionally they’re just like just dialogs, but not modal and don’t require JavaScript.
Marking up popovers
To begin, the popover needs an id as well as the popover attribute with the manual value (which means clicking outside of the popover doesn’t close it), the auto value (clicking outside of the popover does close it), or no value (which means the same thing). To be semantic, the popover can be a .
<dialog id="tooltipA" popover> ... </dialog>
Next, add the popovertarget attribute to the or that we want to toggle the popover’s visibility, with a value matching the popover’s id attribute (this is optional since clicking outside of the popover will close it anyway, unless popover is set to manual):
Place another one of those buttons in your main document, so that you can show the popover. That’s right, popovertarget is actually a toggle (unless you specify otherwise with the popovertargetaction attribute that accepts show, hide, or toggle as its value — more on that later).
Styling popovers
CodePen Embed Fallback
By default, popovers are centered within the top layer (like dialogs), but you probably don’t want them there as they’re not modals, after all.
You can easily pull them into a corner using fixed positioning, but for a tooltip-style popover you’d want it to be relative to the trigger that opens it. CSS Anchor Positioning makes this super easy:
main [popovertarget] {
anchor-name: --trigger;
}
[popover] {
margin: 0;
position-anchor: --trigger;
top: calc(anchor(bottom) + 10px);
justify-self: anchor-center;
}
/* This also works but isn’t needed
unless you’re using the display property
[popover]:popover-open {
...
}
*/
The problem though is that you have to name all of these anchors, which is fine for a tabbed component but overkill for a website with quite a few tooltips. Luckily, we can match an id attribute on the button to an anchor attribute on the popover, which isn’t well-supported as of November 2024 but will do for this demo:
CodePen Embed Fallback
<main>
<!-- The id should match the anchor attribute -->
<button id="anchorA" popovertarget="tooltipA">Show tooltipA</button>
<button id="anchorB" popovertarget="tooltipB">Show tooltipB</button>
</main>
<dialog anchor="anchorA" id="tooltipA" popover>
<button popovertarget="tooltipA">Hide tooltipA</button>
</dialog>
<dialog anchor="anchorB" id="tooltipB" popover>
<button popovertarget="tooltipB">Hide tooltipB</button>
</dialog>
main [popovertarget] { anchor-name: --anchorA; } /* No longer needed */
[popover] {
margin: 0;
position-anchor: --anchorA; /* No longer needed */
top: calc(anchor(bottom) + 10px);
justify-self: anchor-center;
}
The next issue is that we expect tooltips to show on hover and this doesn’t do that, which means that we need to use JavaScript. While this seems complicated considering that we can create tooltips much more easily using ::before/::after/content:, popovers allow HTML content (in which case our tooltips are actually toggletips by the way) whereas content: only accepts text.
Adding JavaScript functionality
Which leads us to this…
CodePen Embed Fallback
Okay, so let’s take a look at what’s happening here. First, we’re using anchor attributes to avoid writing a CSS block for each anchor element. Popovers are very HTML-focused, so let’s use anchor positioning in the same way. Secondly, we’re using JavaScript to show the popovers (showPopover()) on mouseover. And lastly, we’re using JavaScript to hide the popovers (hidePopover()) on mouseout, but not if they contain a link as obviously we want them to be clickable (in this scenario, we also don’t hide the button that hides the popover).
<main>
<button id="anchorLink" popovertarget="tooltipLink">Open tooltipLink</button>
<button id="anchorNoLink" popovertarget="tooltipNoLink">Open tooltipNoLink</button>
</main>
<dialog anchor="anchorLink" id="tooltipLink" popover>Has <a href="#">a link</a>, so we can’t hide it on mouseout
<button popovertarget="tooltipLink">Hide tooltipLink manually</button>
</dialog>
<dialog anchor="anchorNoLink" id="tooltipNoLink" popover>Doesn’t have a link, so it’s fine to hide it on mouseout automatically
<button popovertarget="tooltipNoLink">Hide tooltipNoLink</button>
</dialog>
/* Select and loop all popover triggers */
document.querySelectorAll("main [popovertarget]").forEach((popovertarget) => {
/* Select the corresponding popover */
const popover = document.querySelector(`#${popovertarget.getAttribute("popovertarget")}`);
/* Show popover on trigger mouseover */
popovertarget.addEventListener("mouseover", () => {
popover.showPopover();
});
/* Hide popover on trigger mouseout, but not if it has a link */
if (popover.matches(":not(:has(a))")) {
popovertarget.addEventListener("mouseout", () => {
popover.hidePopover();
});
}
});
At first, I was sure that popovers having backdrops was an oversight, the argument being that they shouldn’t obscure a focusable main document. But maybe it’s okay for a couple of seconds as long as we can resume what we were doing without being forced to close anything? At least, I think this works well for a set of onboarding tips:
CodePen Embed Fallback
<!-- Re-showing ‘A’ rolls the onboarding back to that step -->
<button popovertarget="onboardingTipA" popovertargetaction="show">Restart onboarding</button>
<!-- Hiding ‘A’ also hides subsequent tips as long as the popover attribute equates to auto -->
<button popovertarget="onboardingTipA" popovertargetaction="hide">Cancel onboarding</button>
<ul>
<li id="toolA">Tool A</li>
<li id="toolB">Tool B</li>
<li id="toolC">Another tool, “C”</li>
<li id="toolD">Another tool — let’s call this one “D”</li>
</ul>
<!-- onboardingTipA’s button triggers onboardingTipB -->
<dialog anchor="toolA" id="onboardingTipA" popover>
onboardingTipA <button popovertarget="onboardingTipB" popovertargetaction="show">Next tip</button>
</dialog>
<!-- onboardingTipB’s button triggers onboardingTipC -->
<dialog anchor="toolB" id="onboardingTipB" popover>
onboardingTipB <button popovertarget="onboardingTipC" popovertargetaction="show">Next tip</button>
</dialog>
<!-- onboardingTipC’s button triggers onboardingTipD -->
<dialog anchor="toolC" id="onboardingTipC" popover>
onboardingTipC <button popovertarget="onboardingTipD" popovertargetaction="show">Next tip</button>
</dialog>
<!-- onboardingTipD’s button hides onboardingTipA, which in-turn hides all tips -->
<dialog anchor="toolD" id="onboardingTipD" popover>
onboardingTipD <button popovertarget="onboardingTipA" popovertargetaction="hide">Finish onboarding</button>
</dialog>
/*
After users have had a couple of
seconds to breathe, start the onboarding
*/
setTimeout(() => {
document.querySelector("#onboardingTipA").showPopover();
}, 2000);
Again, let’s unpack. Firstly, setTimeout() shows the first onboarding tip after two seconds. Secondly, a simple fade-in-fade-out background animation runs on the backdrop and all subsequent backdrops. The main document isn’t made inert and the backdrop doesn’t persist, so attention is diverted to the onboarding tips while not feeling invasive.
Thirdly, each popover has a button that triggers the next onboarding tip, which triggers another, and so on, chaining them to create a fully HTML onboarding flow. Typically, showing a popover closes other popovers, but this doesn’t appear to be the case if it’s triggered from within another popover. Also, re-showing a visible popover rolls the onboarding back to that step, and, hiding a popover hides it and all subsequent popovers — although that only appears to work when popover equates to auto. I don’t fully understand it but it’s enabled me to create “restart onboarding” and “cancel onboarding” buttons.
With just HTML. And you can cycle through the tips using esc and return.
Creating modal popovers
Hear me out. If you like the HTML-ness of popover but the semantic value of , this JavaScript one-liner can make the main document inert, therefore making your popovers modal:
However, the popovers must come after the main document; otherwise they’ll also become inert. Personally, this is what I’m doing for modals anyway, as they aren’t a part of the page’s content.
<body>
<!-- All of this will become inert -->
</body>
<!-- Therefore, the modals must come after -->
<dialog popover> ... </dialog>
Aaaand… breathe
Yeah, that was a lot. But…I think it’s important to look at all of these APIs together now that they’re starting to mature, in order to really understand what they can, can’t, should, and shouldn’t be used for. As a parting gift, I’ll leave you with a transition-enabled version of each API:
I’m a bit uncomfortable that responsive design is often pushed into discreet chunks, like “layout A up to this size, then layout B until there’s enough space for layout C.” It’s OK, it works and fits into a workflow where screens are designed as static layouts in PhotoFigVa (caveat, I made that up). But the process feels like a compromise to me. I’ve long believed that responsive design should be almost invisible to the user. When they visit my site on a mobile device while waiting in line for K-Pop tickets, they shouldn’t notice that it’s different from just an hour ago, sitting at the huge curved gaming monitor they persuaded their boss they needed.
Consider this simple hero banner and its mobile equivalent. Sorry for the unsophisticated design. The image is AI generated, but It’s the only thing about this article that is.
The meerkat and the text are all positioned and sized differently. The traditional way to pull this off is to have two layouts, selected by a media, sorry, container query. There might be some flexibility in each layout, perhaps centering the content, and a little fluid typography on the font-size, but we’re going to choose a point at which we flip the layout in and out of the stacked version. As a result, there are likely to be widths near the breakpoint where the layout looks either a little empty or a little congested.
Is there another way?
It turns out there is. We can apply the concept of fluid typography to almost anything. This way we can have a layout that fluidly changes with the size of its parent container. Few users will ever see the transition, but they will all appreciate the results. Honestly, they will.
Let’s get this styled up
For the first step, let’s style the layouts individually, a little like we would when using width queries and a breakpoint. In fact, let’s use a container query and a breakpoint together so that we can easily see what properties need to change.
This is the markup for our hero, and it won’t change:
I’ve attached the background image to a ::before pseudo-element so I can use container queries on it (because containers cannot query themselves). We’ll keep this later on so that we can use inline container query (cqi) units. For now, here’s the container query that just shows the values we’re going to make fluid:
You can see the code running in a live demo — it’s entirely static to show the limitations of a typical approach.
Let’s get fluid
Now we can take those start and end points for the size and position of both the text and background and make them fluid. The text size uses fluid typography in a way you are already familiar with. Here’s the result — I’ll explain the expressions once you’ve looked at the code.
First the changes to the position and size of the text:
/* Line changes
* -12,27 +12,32
*/
.details {
/* ... lines 14-16 unchanged */
/* Evaluates to 50px for a 360px wide container, and 220px for 1200px */
top: clamp(50px, 20.238cqi - 22.857px, 220px);
/* Evaluates to 20px for a 360px wide container, and 565px for 1200px */
left: clamp(20px, 64.881cqi - 213.571px, 565px);
/* ... lines 20-25 unchanged */
h1 {
/* Evaluates to 3.5rem for a 360px wide container, and 5rem for 1200px */
font-size: clamp(3.5rem, 2.857rem + 2.857cqi, 5rem);
/* ... font-weight unchanged */
}
p {
/* Evaluates to 2rem for a 360px wide container, and 2.5rem for 1200px */
font-size: clamp(2rem, 1.786rem + 0.952cqi, 2.5rem);
}
}
And here’s the background position and size for the meerkat image:
/* Line changes
* -50,3 +55,8
*/
/* Evaluates to -310px for a 360px wide container, and 0px for 1200px */
background-position-x: clamp(-310px, 36.905cqi - 442.857px, 0px);
/* Evaluates to -25px for a 360px wide container, and 0px for 1200px */
background-position-y: clamp(-25px, 2.976cqi);
/* Evaluates to 710px for a 360px wide container, and 589px for 1200px */
background-size: auto clamp(589px, 761.857px - 14.405cqi, 710px);
Now we can drop the container query entirely.
Let’s explain those clamp() expressions. We’ll start with the expression for the top property.
/* Evaluates to 50px for a 360px wide container, and 220px for 1200px */
top: clamp(50px, 20.238cqi - 22.857px, 220px);
You’ll have noticed there’s a comment there. These expressions are a good example of how magic numbers are a bad thing. But we can’t avoid them here, as they are the result of solving some simultaneous equations — which CSS cannot do!
The upper and lower bounds passed to clamp() are clear enough, but the expression in the middle comes from these simultaneous equations:
f + 12v = 220
f + 3.6v = 50
…where f is the number of fixed-size length units (i.e., px) and v is the variable-sized unit (cqi). In the first equation, we are saying that we want the expression to evaluate to 220px when 1cqi is equal to 12px. In the second equation, we’re saying we want 50px when 1cqi is 3.6px, which solves to:
f = -22.857
v = 20.238
…and this tidies up to 20.238cqi – 22.857px in a calc()-friendly expression.
When the fixed unit is different, we must change the size of the variable units accordingly. So for the
element’s font-size we have;
/* Evaluates to 2rem for a 360px wide container, and 2.5rem for 1200px */
font-size: clamp(2rem, 1.786rem + 0.952cqi, 2.5rem);
This is solving these equations because, at a container width of 1200px, 1cqi is the same as 0.75rem (my rems are relative to the default UA stylesheet, 16px), and at 360px wide, 1cqi is 0.225rem.
f + 0.75v = 2.5
f + 0.225v = 2
This is important to note: The equations are different depending on what unit you are targeting.
Honestly, this is boring math to do every time, so I made a calculator you can use. Not only does it solve the equations for you (to three decimal places to keep your CSS clean) it also provides that helpful comment to use alongside the expression so that you can see where they came from and avoid magic numbers. Feel free to use it. Yes, there are many similar calculators out there, but they concentrate on typography, and so (rightly) fixate on rem units. You could probably port the JavaScript if you’re using a CSS preprocessor.
The clamp() function isn’t strictly necessary at this point. In each case, the bounds of clamp() are set to the values of when the container is either 360px or 1200px wide. Since the container itself is constrained to those limits — by setting min-width and max-width values — the clamp() expression should never invoke either bound. However, I prefer to keep clamp() there in case we ever change our minds (which we are about to do) because implicit bounds like these are difficult to spot and maintain.
Avoiding injury
We could consider our work finished, but we aren’t. The layout still doesn’t quite work. The text passes right over the top of the meerkat’s head. While I have been assured this causes the meerkat no harm, I don’t like the look of it. So, let’s make some changes to make the text avoid hitting the meerkat.
The first is simple. We’ll move the meerkat to the left more quickly so that it gets out of the way. This is done most easily by changing the lower end of the interpolation to a wider container. We’ll set it so that the meerkat is fully left by 450px rather than down to 360px. There’s no reason the start and end points for all of our fluid expressions need to align with the same widths, so we can keep the other expressions fluid down to 360px.
Using my trusty calculator, all we need to do is change the clamp() expressions for the background-position properties:
/* Line changes
* -55,5 +55,5
*/
/* Evaluates to -310px for a 450px wide container, and 0px for 1200px */
background-position-x: clamp(-310px, 41.333cqi - 496px, 0px);
/* Evaluates to -25px for a 450px wide container, and 0px for 1200px */
background-position-y: clamp(-25px, 3.333cqi - 40px, 0px);
This improves things, but not totally. I don’t want to move it any quicker, so next we’ll look at the path the text takes. At the moment it moves in a straight line, like this:
But can we bend it? Yes, we can.
A Bend in the path
One way we can do this is by defining two different interpolations for the top coordinate that places the line at different angles and then choosing the smallest one. This way, it allows the steeper line to “win” at larger container widths, and the shallower line becomes the value that wins when the container is narrower than about 780px. The result is a line with a bend that misses the meerkat.
All we’re changing is the top value, but we must calculate two intermediate values first:
/* Line changes
* -18,2 +18,9 @@
*/
/* Evaluates to 220px for a 1200px wide container, and -50px for 360px */
--top-a: calc(32.143cqi - 165.714px);
/* Evaluates to 120px for a 1200px wide container, and 50px for 360px */
--top-b: calc(20px + 8.333cqi);
/* By taking the max, --topA is used at lower widths, with --topB taking over when wider.
We only need to apply clamp when the value is actually used */
top: clamp(50px, max(var(--top-a), var(--top-b)), 220px);
For these values, rather than calculating them formally using a carefully chosen midpoint, I experimented with the endpoints until I got the result I wanted. Experimentation is just as valid as calculation as a way of getting the result you need. In this case, I started with duplicates of the interpolation in custom variables. I could have split the path into explicit sections using a container query, but that doesn’t reduce the math overhead, and using the min() function is cleaner to my eye. Besides, this article isn’t strictly about container queries, is it?
Now the text moves along this path. Open up the live demo to see it in action.
CSS can’t do everything
As a final note on the calculations, it’s worth pointing out that there are restrictions as far as what we can and can’t do. The first, which we have already mitigated a little, is that these interpolations are linear. This means that easing in or out, or other complex behavior, is not possible.
Another major restriction is that CSS can only generate length values this way, so there is no way in pure CSS to apply, for example, opacity or a rotation angle that is fluid based on the container or viewport size. Preprocessors can’t help us here either because the limitation is on the way calc() works in the browser.
Both of these restrictions can be lifted if you’re prepared to rely on a little JavaScript. A few lines to observe the width of the container and set a CSS custom property that is unitless is all that’s needed. I’m going to use that to make the text follow a quadratic Bezier curve, like this:
There’s too much code to list here, and too much math to explain the Bezier curve, but go take a look at it in action in this live demo.
We wouldn’t even need JavaScript if expressions like calc(1vw / 1px) didn’t fail in CSS. There is no reason for them to fail since they represent a ratio between two lengths. Just as there are 2.54cm in 1in, there are 8px in 1vw when the viewport is 800px wide, so calc(1vw / 1px) should evaluate to a unitless 8 value.
They do fail though, so all we can do is state our case and move on.
Fluid everything doesn’t solve all layouts
There will always be some layouts that need size queries, of course; some designs will simply need to snap changes at fixed breakpoints. There is no reason to avoid that if it’s right. There is also no reason to avoid mixing the two, for example, by fluidly sizing and positioning the background while using a query to snap between grid definitions for the text placement. My meerkat example is deliberately contrived to be simple for the sake of demonstration.
One thing I’ll add is that I’m rather excited by the possibility of using the new Anchor Positioning API for fluid positioning. There’s the possibility of using anchor positioning to define how two elements might flow around the screen together, but that’s for another time.
They’re out! Like many of you, I look forward to these coming out each year. I don’t put much stock in surveys but they can be insightful and give a snapshot of the CSS zeitgeist. There are a few little nuggets in this year’s results that I find interesting. But before I get there, you’ll want to also check out what others have already written about it.
Oh, I guess that’s it — at least it’s the most formal write-up I’ve seen. There’s a little summary by Ahmad Shadeed at the end of the survey that generally rounds things up. I’ll drop in more links as I find ’em.
In no particular order…
Demographics
Josh has way more poignant thoughts on this than I do. He rightfully calls out discrepancies in gender pay and regional pay, where men are way more compensated than women (a nonsensical and frustratingly never-ending trend) and the United States boasts more $100,000 salaries than anywhere else. The countries with the highest salaries were also the most represented in survey responses, so perhaps the results are no surprise. We’re essentially looking at a snapshot of what it’s like to be a rich, white male developer in the West.
Besides pay, my eye caught the Age Group demographics. As an aging front-ender, I often wonder what we all do when we finally get to retirement age. I officially dropped from the most represented age group (30-39, 42%) a few years ago into the third most represented tier (40-49, 21%). Long gone are my days being with the cool kids (20-29, 27%).
And if the distribution is true to life, I’m riding fast into my sunset years and will be only slightly more represented than those getting into the profession. I don’t know if anyone else feels similarly anxious about aging in this industry — but if you’re one of the 484 folks who identify with the 50+ age group, I’d love to talk with you.
Before we plow ahead, I think it’s worth calling out how relatively “new” most people are to front-end development.
Wow! Forty-freaking-four percent of respondents have less than 10 years of experience. Yes, 10 years is a high threshold, but we’re still talking about a profession that popped up in recent memory.
For perspective, someone developing for 10 years came to the field around 2014. That’s just when we were getting Flexbox, and several years after the big bang of CSS 3 and HTML 5. That’s just under half of developers who never had to deal with the headaches of table layouts, clearfix hacks, image sprites, spacer images, and rasterized rounded corners. Ethan Marcotte’s seminal article on “Responsive Web Design” predates these folks by a whopping four years!
I’m not entirely sure what to make of this section. When there are so many CSS features, how do you determine which are most widely used? How do you pare it down to just 50 features? Like, are filter effects really the most widely used CSS feature? So many questions, but the results are always interesting nonetheless.
What I find most interesting are the underused features. For example, hanging-punctuation comes in dead last in usage (1.57%) but is the feature that most developers (52%) have on their reading list. (If you need some reading material on it, Chris initially published the Almanac entry for hanging-punctuation back in 2013.)
I also see Anchor Positioning at the end of the long tail with reported usage at 4.8%. That’ll go up for sure now that we have at least one supporting browser engine (Chromium) but also given all of the tutorials that have sprung up in the past few months. Yes, we’ve contributed to that noise… but it’s good noise! I think Juan published what might be the most thorough and thoughtful guide on the topic yet.
I’m excited to see Cascade Layers falling smack dab in the middle of the pack at a fairly robust 18.7%. Cascade Layers are super approachable and elegantly designed that I have trouble believing anybody these days when they say that the CSS Cascade is difficult to manage. And even though @scope is currently low on the list (4.8%, same as Anchor Positioning), I’d bet the crumpled gum wrapper in my pocket that the overall sentiment of working with the Cascade will improve dramatically. We’ll still see “CSS is Awesome” memes galore, but they’ll be more like old familiar dad jokes in good time.
Interestingly enough, viewport units come in at Number 11 with 44.2% usage… which lands them at Number 2 for most experience that developers have with CSS layout. Does that suggest that layout features are less widely used than CSS filters? Again, so many questions.
Frameworks
How many of you were surprised that Tailwind blew past Bootstrap as Top Dog framework in CSS Land? Nobody, right?
More interesting to me is that “No CSS framework” clocks in at Number 13 out of 21 list frameworks. Sure, its 46 votes are dwarfed by the 138 for Material UI at Number 10… but the fact that we’re seeing “no framework” as a ranking option at all would have been unimaginable just three years ago.
The same goes for CSS pre/post-processing. Sass (67%) and PostCSS (38%) are the power players, but “None” comes in third at 19%, ahead of Less, Stylus, and Lightning CSS.
It’s a real testament to the great work the CSSWG is doing to make CSS better every day. We don’t thank the CSSWG enough — thank you, team! Y’all are heroes around these parts.
CSS Usage
Josh already has a good take on the fact that only 67% of folks say they test their work on mobile phones. It should be at least tied with the 99% who test on desktops, right? Right?! Who knows, maybe some responses consider things like “Responsive Design Mode” desktop features to be the equivalent of testing on real mobile devices. I find it hard to believe that only 67% of us test mobile.
Oh, and The Great Divide is still alive and well if the results are true and 53% write more JavsScript than CSS in their day-to-day.
This is where I get to toot our own horn a bit because CSS-Tricks continues to place first among y’all when it comes to the blogs you follow for CSS happenings.
I’m also stoked to see Smashing Magazine right there as well. It was fifth in 2023 and I’d like to think that rise is due to me joining the team last year. Correlation implies causation, amirite?
But look at Kevin Powell and Josh in the Top 10. That’s just awesome. It speaks volumes about their teaching talents and the hard work they put into “helping people fall in love with CSS” as Kevin might say it. I was able to help Kevin with a couple of his videos last year (here’s one) and can tell you the guy cares a heckuva lot about making CSS approachable and fun.
Honestly, the rankings are not what we live for. Now that I’ve been given a second wind to work on CSS-Tricks, all I want is to publish things that are valuable to your everyday work as front-enders. That’s traditionally happened as a stream of daily articles but is shifting to more tutorials and resources, whether it’s guides (we’ve published four new ones this year), taking notes on interesting developments, spotlighting good work with links, or expanding the ol’ Almanac to account for things like functions, at-rules, and pseudos (we have lots of work to do).
My 2024 Pick
No one asked my opinion but I’ll say it anyway: Personal blogging. I’m seeing more of us in the front-end community getting back behind the keyboards of their personal websites and I’ve never been subscribed to more RSS feeds than I am today. Some started blogging as a “worry stone” during the 2020 lockdown. Some abandoned socials when Twitter X imploded. Some got way into the IndieWeb. Webrings and guestbooks are even gaining new life. Sure, it can be tough keeping up, but what a good problem to have! Let’s make RSS king once and for all.
That’s a wrap!
Seriously, a huge thanks to Sacha Greif and the entire Devographics team for the commitment to putting this survey together every year. It’s always fun. And the visualizations are always to die for.
You could provide contextual information, like stating the number of notifications, by giving a space-separated list of ids to aria-labelledby.
<button aria-labelledby="notifications-count notifications-label">
<!-- bell icon here -->
<span id="notifications-count">3</span>
</button>
<div role="tooltip" id="notifications-label">Notifications</div>
Providing contextual description
If your tooltip provides a contextual description of the icon, you should use aria-describedby. But, when you do this, you also need to provide an accessible name for the icon.
In this case, Heydon recommends including the label as the text content of the button. This label would be hidden visually from sighted users but read for screen readers.
Then, you can add aria-describedby to include the auxiliary description.
<button class="notifications" aria-describedby="notifications-desc">
<!-- icon for bell here -->
<span id="notifications-count">3</span>
<span class="visually-hidden">Notifications</span>
</button>
<div role="tooltip" id="notifications-desc">View and manage notifications settings</div>
Here, screen readers would say “3 notifications” first, followed by “view and manage notifications settings” after a brief pause.
Additional tooltip dos and don’ts
Here are a couple of additional points you should be aware of:
Do:
Use aria-labellebdy or aria-describedby attributes depending on the type of tooltip you’re building.
Use the tooltip role even if it
In this article, I try to summarize the best practices mentioned by various accessibility experts and their work (like this, this, and this) into a single article that’s easy to read, understand, and apply.
Let’s begin.
What are tooltips?
Tooltips are used to provide simple text hints for UI controls. Think of them as tips for tools. They’re basically little bubbles of text content that pop up when you hover over an unnamed control (like the bell icon in Stripe).
If you prefer more of a formal definition, Sarah Highley provides us with a pretty good one:
A “tooltip” is a non-modal (or non-blocking) overlay containing text-only content that provides supplemental information about an existing UI control. It is hidden by default, and becomes available on hover or focus of the control it describes.
She further goes on to say:
That definition could even be narrowed down even further by saying tooltips must provide only descriptive text.
This narrowed definition is basically (in my experience) how every accessibility expert defines tooltips:
Heydon Pickering takes things even further, saying: If you’re thinking of adding interactive content (even an ok button), you should be using dialog instead.
If your tooltip is used to label an icon — using only one or two words — you should use the aria-labelledby attribute to properly label it since it is attached to nothing else on the page that would help identify it.
You could provide contextual information, like stating the number of notifications, by giving a space-separated list of ids to aria-labelledby.
<button aria-labelledby="notifications-count notifications-label">
<!-- bell icon here -->
<span id="notifications-count">3</span>
</button>
<div role="tooltip" id="notifications-label">Notifications</div>
Providing contextual description
If your tooltip provides a contextual description of the icon, you should use aria-describedby. But, when you do this, you also need to provide an accessible name for the icon.
In this case, Heydon recommends including the label as the text content of the button. This label would be hidden visually from sighted users but read for screen readers.
Then, you can add aria-describedby to include the auxiliary description.
<button class="notifications" aria-describedby="notifications-desc">
<!-- icon for bell here -->
<span id="notifications-count">3</span>
<span class="visually-hidden">Notifications</span>
</button>
<div role="tooltip" id="notifications-desc">View and manage notifications settings</div>
Here, screen readers would say “3 notifications” first, followed by “view and manage notifications settings” after a brief pause.
Additional tooltip dos and don’ts
Here are a couple of additional points you should be aware of:
Do:
Use aria-labellebdy or aria-describedby attributes depending on the type of tooltip you’re building.
Don’t use the aria-haspopup attribute with the tooltip role because aria-haspopup signifies interactive content while tooltip should contain non-interactive content.
Don’t include essential content inside tooltips because some screen readers may ignore aria-labelledby or aria-describedby. (It’s rare, but possible.)
Tooltip limitations and alternatives
Tooltips are inaccessible to most touch devices because:
users cannot hover over a button on a touch device, and
users cannot focus on a button on a touch device.
The best alternative is not to use tooltips, and instead, find a way to include the label or descriptive text in the design.
If the “tooltip” contains a lot of content — including interactive content — you may want to display that information with a Toggletip (or just use a element).
Toggletips exist to reveal information balloons. Often they take the form of little “i” icons.
These informational icons should be wrapped within a element. When opened, screen readers can announce the text contained in it through a live region with the status role.
Speaking anymore about toggletips detracts this article from tooltips so I’ll point you to Heydon’s “Tooltips and Toggletips” article if you’re interested in chasing this short rabbit hole.
That’s all you need to know about tooltips and their current best practices!