CSS Utility Tricks: Invisible Text and Filters
Two utility recipes that come up over and over: hiding content correctly (visually-hidden but readable to screen readers, plus a decision matrix for the four ways to make something disappear) and using `filter` for cheap visual effects like blur, grayscale, and drop-shadow. Both are short on their own but easy to get wrong, so they live together as a quick reference.
471 views
11
/* The .sr-only / .visually-hidden recipe.
Hides the element visually but keeps it in the accessibility tree so
screen readers and search engines still see it. */
.sr-only,
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Variant that becomes visible when focused (skip-to-main-content links). */
.sr-only-focusable:not(:focus):not(:focus-within) {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Example HTML where this matters:
<button>
<span class="sr-only">Close modal</span>
<svg aria-hidden="true">...</svg>
</button>
The visible label is the icon, but assistive tech reads "Close modal". */The visually-hidden technique is the standard pattern for making content invisible without removing it from the accessibility tree. Crucially, it uses position: absolute, a 1x1 size, and clip: rect(0,0,0,0) rather than display: none, because display: none removes the element entirely; screen readers will not see it. Use this every time an icon-only button needs a label, or when you want extra context (like "opens in a new tab") that a sighted user does not need. The focusable variant is for skip-links: hidden by default, but visible when a keyboard user tabs to them, which is a baseline accessibility requirement.
/* display: none
- Removes the element from layout AND the accessibility tree.
- The element does not take any space, cannot be tabbed to.
- Use for: content that should be completely gone (collapsed sections,
conditionally-rendered components). */
.is-hidden {
display: none;
}
/* visibility: hidden
- Element still takes its space in layout (you keep the gap).
- Hidden from screen readers and not focusable.
- Use for: animation start states, placeholder slots that should reserve space. */
.is-invisible {
visibility: hidden;
}
/* opacity: 0
- Element is fully transparent but still in layout AND still focusable.
- Screen readers MAY still announce it depending on user agent.
- Use for: pure visual fade transitions. Pair with pointer-events: none and
aria-hidden="true" if you also need it removed from interaction. */
.is-faded {
opacity: 0;
pointer-events: none;
}
/* aria-hidden="true" attribute on the HTML, not CSS
- Hides from the accessibility tree only.
- Element is still visible, still in layout, still focusable.
- Use for: decorative icons, redundant labels. */
/* Decision rule of thumb:
- Want it gone for everyone? -> display: none
- Want the space but no content? -> visibility: hidden
- Want it animated to invisible? -> opacity: 0 + pointer-events: none
- Want it visible but ignored by AT? -> aria-hidden="true" in HTML
- Want it visible-only-to-AT (icon label)? -> .sr-only / .visually-hidden */These four hiding techniques behave very differently in layout, focus, and accessibility, and choosing the wrong one is a common bug. display: none removes the element entirely from both layout and the a11y tree; visibility: hidden keeps the layout slot but hides the content from sight and from screen readers; opacity: 0 is purely visual, so the element is still focusable and may still announce, which is why login forms that fade out a panel still trap keyboard focus inside the invisible element. aria-hidden="true" is the inverse of the visually-hidden recipe in accordion 1: visible to eyes, hidden to screen readers. Treat the comment block at the bottom as a quick lookup when you are deciding which to use.
/* filter accepts a space-separated list of filter functions. They apply in order. */
/* Blur: useful for backdrops, modal underlays, and skeleton loading states. */
.blur-bg {
filter: blur(4px);
}
/* Grayscale: 0% = full color, 100% = full gray. Animate to highlight. */
.thumb {
filter: grayscale(100%);
transition: filter 0.2s ease;
}
.thumb:hover {
filter: grayscale(0%);
}
/* Drop-shadow: the filter version respects the shape of the rendered pixels,
which is critical for SVG icons and PNGs with transparency. box-shadow only
sees the bounding box. */
.icon {
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
}
/* Combine multiple filters in one declaration. Order matters: the second
filter operates on the output of the first. */
.faded-bw {
filter: grayscale(100%) opacity(0.6) blur(1px);
}
/* backdrop-filter applies to the area BEHIND the element, not the element
itself. Pair with a partially-transparent background for frosted-glass UI. */
.glass-panel {
background: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(8px) saturate(1.1);
}
/* Cost note: filter and backdrop-filter promote the element to its own
compositing layer, which uses GPU memory. Apply sparingly to avoid jank,
especially on mobile, and never to large containers that frequently repaint. */filter is one of the highest-leverage CSS properties: a single declaration produces effects (blur, grayscale, sepia, drop-shadow, hue-rotate) that would otherwise need an SVG filter or a Canvas. The killer feature for icons is filter: drop-shadow(...) which respects the alpha channel of the rendered pixels, so a transparent SVG silhouette gets a shadow shaped like the silhouette, not its bounding box. backdrop-filter is the modern frosted-glass effect; it applies to the area behind the element, so it needs a translucent background to be visible. The catch is performance: every element with a filter becomes its own compositing layer, which costs GPU memory and can cause jank on low-end devices when applied to large or frequently-repainting elements.
