It is a pervasive conflict in UI architecture: your design system requires cards with rounded corners and full-bleed images, necessitating overflow: hidden and border-radius. However, your accessibility requirements dictate a high-contrast focus indicator that extends beyond the component's perimeter.
When you apply overflow: hidden to a container, it creates a hard clipping boundary. While standard CSS outline is painted outside the border edge, specific layout contexts (such as grid containers with their own overflow handling or transform layers) or the desire for inset focus styles often result in the focus indicator being chopped off or obscured by child content (like images).
This results in a direct violation of WCAG 2.4.7 (Focus Visible) and often fails WCAG 2.4.13 (Focus Appearance) regarding contrast and size requirements.
Here is the architectural root cause and the robust, component-level fix.
The Root Cause: Paint Order and Clipping Contexts
To understand why this breaks, we must look at the CSS Box Model and Stacking Contexts.
- The Clipping Boundary:
overflow: hiddenclips content to the padding box. - The Image Layer: When you have a full-bleed image inside a card, the image sits on the content layer.
- The Conflict:
- If you use an inset focus ring (e.g.,
outline-offset: -4px) to "tuck" the ring inside the design, the image (which has a higher stacking order or simply sits on top of the background) will occlude the ring. - If you use an outset focus ring (standard behavior), it generally renders. However, if the card itself is placed inside a container with
overflow: hidden(like a carousel or a tight grid track), the ring is clipped.
- If you use an inset focus ring (e.g.,
The most common "hack" is using z-index or negative margins, but these are brittle. The correct engineering solution requires separating the Interactive Shell from the Visual Canvas.
The Solution: The "Interactive Shell" Pattern
The most robust way to solve this is to decouple the element receiving focus from the element enforcing the clipping. We effectively create a parent "Shell" that handles interaction and focus rings (with visible overflow), and a child "Canvas" that handles the border-radius and image containment.
The Architecture
.card-shell: The interactive element (<a>or<button>). It has nooverflow: hidden. It handles thefocus-visiblestate..card-visual: The internal wrapper. It handlesborder-radius,overflow: hidden, and background styles..card-focus-ring: A pseudo-element on the shell used to draw the ring, ensuring it sits above all clipped content (optional, but recommended for custom styling).
Implementation
Here is a modern, WCAG-compliant implementation using CSS Nesting and Logical Properties.
<article class="card-component">
<!-- 1. The Interactive Shell -->
<a href="/post/css-architecture" class="card-shell">
<!-- 2. The Visual Canvas (Clipped) -->
<div class="card-visual">
<img
src="illustration.jpg"
alt="Abstract 3d rendering of code blocks"
class="card-image"
width="600"
height="400"
/>
<div class="card-content">
<h3>CSS Architecture</h3>
<p>Decoupling logic from aesthetics.</p>
</div>
</div>
</a>
</article>
:root {
--radius-md: 12px;
--focus-color: #2563eb; /* Accessible Blue */
--focus-width: 3px;
--focus-offset: 4px;
}
/*
1. The Interactive Shell
- Receives keyboard focus
- NO overflow hidden here
- Position relative to anchor the focus pseudo-element
*/
.card-shell {
display: block;
position: relative;
text-decoration: none;
color: inherit;
border-radius: var(--radius-md);
/*
Performance optimization: Promotes to its own layer
to prevent repaints on focus state changes
*/
will-change: transform;
}
/*
2. The Focus Indicator
- We use a pseudo-element to ensure the ring sits ON TOP
of the image and isn't clipped by the internal visual container.
*/
.card-shell:focus-visible::after {
content: "";
position: absolute;
inset: calc(var(--focus-offset) * -1); /* Expands outward */
border: var(--focus-width) solid var(--focus-color);
border-radius: calc(var(--radius-md) + var(--focus-offset));
pointer-events: none; /* Let clicks pass through to the link */
z-index: 10; /* Ensures visibility above all internal content */
/* Optional: Add a white ring between card and focus for contrast on dark backgrounds */
box-shadow: 0 0 0 2px white;
}
/* Hide default outline since we are replacing it */
.card-shell:focus-visible {
outline: none;
}
/*
3. The Visual Canvas
- Handles the aesthetic shape and clipping
- Separate from the focus logic
*/
.card-visual {
position: relative;
overflow: hidden; /* This clips the image */
border-radius: var(--radius-md);
background: #fff;
border: 1px solid #e5e7eb;
transition: transform 0.2s ease;
/* Fix for Safari border-radius clipping bug with transforms */
isolation: isolate;
}
/* Interaction: Visuals scale, but focus ring stays stable */
.card-shell:hover .card-visual {
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1);
}
.card-image {
display: block;
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}
.card-content {
padding: 1.5rem;
}
Why This Works
1. Separation of Concerns
By moving overflow: hidden from the anchor tag (.card-shell) to an inner wrapper (.card-visual), the anchor tag remains an unclipped context. The focus ring (generated by the ::after pseudo-element on the shell) exists in the shell's coordinate space, not the clipped visual space.
2. Z-Index Management
The common failure point is an image covering an inset focus ring. By using position: absolute and z-index: 10 on the pseudo-element, we force the focus ring into a stacking context above the .card-visual (and consequently, the image).
3. WCAG Compliance
- Visibility: The ring is guaranteed to be visible because it sits outside the clipping boundary.
- Contrast: The code includes a
box-shadowspacer (white ring) inside the colored border. This ensures that if the card is placed on a blue background (matching the focus color), the white spacer maintains the 3:1 contrast ratio required against adjacent colors.
Conclusion
Stop trying to force overflow: hidden and focus rings to coexist on the same DOM node. It is a battle against the browser's painting logic that you will eventually lose in edge cases.
Adopt the Shell/Canvas pattern. It adds one level of nesting to your DOM, but in exchange, it provides a bulletproof foundation for accessible, interactive cards that support any visual design requirement without compromising WCAG standards.