惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Tenable Blog
博客园_首页
Vercel News
Vercel News
WordPress大学
WordPress大学
美团技术团队
G
Google Developers Blog
大猫的无限游戏
大猫的无限游戏
小众软件
小众软件
Y
Y Combinator Blog
博客园 - 【当耐特】
量子位
酷 壳 – CoolShell
酷 壳 – CoolShell
The Cloudflare Blog
T
The Blog of Author Tim Ferriss
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Google DeepMind News
Google DeepMind News
云风的 BLOG
云风的 BLOG
腾讯CDC
M
MIT News - Artificial intelligence
爱范儿
爱范儿
Recent Announcements
Recent Announcements
雷峰网
雷峰网
Last Week in AI
Last Week in AI
宝玉的分享
宝玉的分享
The Register - Security
The Register - Security
Jina AI
Jina AI
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Hugging Face - Blog
Hugging Face - Blog
P
Privacy & Cybersecurity Law Blog
Recorded Future
Recorded Future
Help Net Security
Help Net Security
N
News and Events Feed by Topic
博客园 - Franky
P
Proofpoint News Feed
L
LINUX DO - 热门话题
S
SegmentFault 最新的问题
The GitHub Blog
The GitHub Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
月光博客
月光博客
D
Docker
Google DeepMind News
Google DeepMind News
有赞技术团队
有赞技术团队
IT之家
IT之家
Security Latest
Security Latest
L
LangChain Blog
V
V2EX
阮一峰的网络日志
阮一峰的网络日志
J
Java Code Geeks

Master.dev Blog RSS Feed

Thinking Horizontally in CSS @layer Boundary-Aware Styling in CSS The Field Guide to Grid Lanes Cloudflare Workers and Hyperdrive with TanStack Start Totally Free Course: Claude Code Uber for Dogs: How to Stop & Think for Design Fluid Typography with progress() Introduction to Cloudflare Workers for Web Apps Modern Web Guidance Let’s Play With Gap Decorations!
How to Make an Interactive Element Invisible but Accessible
Daniel Schwarz on July 8, 2026 · 2026-07-08 · via Master.dev Blog RSS Feed

Let’s say we want to hide some interactive elements until interaction occurs. For example, we want some cards to have share buttons, but we don’t want them to clutter the UI. Other use-cases include skip links, which we can reveal at key moments in the tab order, anchor links (i.e., links to a specific part of a page), which we can reveal when we’re within said part, copy-to-clipboard buttons, which we can reveal when we’re somehow engaged with the content that we want to copy, scroll-to-top buttons, actionbar items of lesser importance — you know, things that might be okay to hide initially because they’re highly contextual or because users will know that they’re there.

This means we need to hide the element while still making it accessible. Well, that’s what the hidden="until-found" attribute-value does, but we also need to reserve the space, since we can’t hover what isn’t there, right? We also don’t want to cause any layout shift.

And that’s exactly where contain-intrinsic-size: auto none comes into it, which remembers the size of a rendered element.

Here’s an example:

The red border outlines the button, which is hidden but accessible (hoverable, focusable, find-in-pageable, and fragment navigable). The hidden=until-found attribute-value makes it find-in-pageable and fragment navigable, while a bit of CSS makes it hoverable and focusable. If you’re ready to see how, let’s begin.

The HTML (hidden=until-found)

In the demo below, I’ve added hidden=until-found to a <button>. This makes the button’s content find-in-pageable (we can reveal the text by searching for “Hidden until found”), fragment navigable (it’ll reveal itself if it matches a fragment identifier/URL hash), and it’s also focusable right off the bat (well…the button’s focusable, not the content).

<button hidden="until-found">Hidden until found</button>Code language: HTML, XML (xml)

hidden=until-found declares content-visibility: hidden, overwriting the initial value of visible, so it shouldn’t come as a surprise that it toggles the content’s visibility, not the element itself. This is why we see an empty button to begin with, and why the button remains focusable.

We’ll worry about hiding the overall button in a minute. For now, let’s worry about the fact that interacting with it causes layout shift, and that it’s not fully hoverable, which’ll be a bigger issue once we actually hide it. Essentially, we need the button to be in its full-size state with the content hidden.

Other approaches (visibility: hiddendisplay: noneopacity: 0width: 0 + height: 0, etc.) aren’t accessible visually, or to screen readers, or to find-in-page. Old-school opacity: 0 is actually the most effective of those because it reserves layout space and makes the content accessible to screen readers, but when it comes to find-in-page, the content is found but not shown. hidden=until-found, which leverages content-visibility, works because it finds the content and toggles it.

The CSS (mostly contain-intrinsic-size: auto none)

But again, how do we reserve the space to make it fully hoverable and avoid layout shift? How do we hover something that isn’t there, exactly? Well, contain-intrinsic-size: auto none saves the last rendered size of an element, so all we need to do is render it, if only for a millisecond. We can do this using a @keyframes animation.

Obviously, we don’t need to do this if we know size of the element already and have declared it, but if we don’t, then we can show the element for a millisecond using a @keyframes animation and use contain-intrinsic-size: auto none to say, “Hey, remember that it’s this size.”

Anyway, we can see our @keyframes animation below, where content-visibility: visible is declared at the start of the animation. We don’t include a to keyframe selector, which means that it’ll animate to the content-visibility: hidden fallback, courtesy of hidden=until-found.

@keyframes render {
  /* When the animation starts */
  from {
    /* Make the content visible */
    content-visibility: visible;
  }

  /* This is already implied
  to {
    content-visibility: hidden;
  }
  */
}Code language: CSS (css)

Now, let’s apply this animation to the button and ensure that its dimensions are saved during the very brief moment that it’s rendered.

animation: 1ms render applies content-visibility: visible during the one-millisecond animation as contain-intrinsic-size: auto none saves the size. However, you’ll almost certainly see the content flicker regardless. From my understanding, this is because of how the Critical Rendering Path (CRP) works, but we can hide the element altogether with opacity: 0, so no worries there.

button {
  /* This is almost instantaneous */
  animation: 1ms render;

  /* Save the rendered size while visible */
  contain-intrinsic-size: auto none;

  /* Prevent flickering during the animation */
  opacity: 0;
}Code language: CSS (css)

On the topic of rendering and depending on web performance, 1ms might not be enough time to capture the size, especially in Safari for some reason. So if the technique doesn’t work, try increasing the animation duration. Even 2ms or 3ms can make a big difference.

As the demo below shows, the button is fully laid out (as indicated by the container’s red border). It’s invisible, and the content is hidden, but it’s also hoverable, focusable, find-in-pageable, and fragment navigable.

All we need to do now is make the button react to those interactions and behaviors, which is what I’ve done below with the &:is(:hover, :focus, :not([hidden])) nested selector:

button {
  /* This is almost instantaneous */
  animation: 1ms render;

  /* Save the rendered size while visible */
  contain-intrinsic-size: auto none;

  /* Prevent flickering during the animation */
  opacity: 0;

  /* Reveal it at will */
  &:is(:hover, :focus, :not([hidden])) {
    opacity: 1;
    content-visibility: visible;
  }
}Code language: CSS (css)

Here, opacity (which was set to 0 to hide the flickering) is set to 1, while content-visibility (which was set to hidden by hidden=until-found) is set to visible. This happens on hover, on focus, and when the hidden attribute is toggled off by find-in-page or a fragment identifier. If you feel like you’re playing too fast and loose with the whole “users will know it’s there” thing, you can move that :hover to a parent (i.e., have a link-to-this-comment button reveal itself when the overall comment is hovered).

The button already reverts back to content-visibility: visible when find-in-page or a fragment identifier removes the hidden attribute (i.e., :not([hidden])), but for brevity, we can keep it as-is.

By the way, if the browser has to wait for CSS (e.g., because it’s external), the layout will shift regardless, so I recommend serving the CSS with the HTML, just as I’ve done in the demos.

Final Thoughts

I don’t know if making interactive elements (or anything, really) invisible is a neat idea, but if you’re being forced to or you intend to do it anyway, this approach at least responds to all relevant states of interaction, whereas other techniques don’t.

Having said that, I imagine you’d use this technique for decluttering (i.e., to deprioritize unimportant UI components). With that in mind, I strongly suggest placing those components exactly where users expect them, perhaps with some extra padding to make it easier to trigger the hover state.

Finally, note that there are some quirks that look like bugs, but aren’t:

  • Content found by find-in-page doesn’t disappear once found
  • In Safari, buttons are only tabbable with tab and option (by default, anyway)
  • The technique won’t work in background tabs unless they’re prerendered