More Cards Snippets
Focus Cards — Free HTML CSS JS Hover Blur Grid Snippet
Focus Cards · Cards · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Focus Cards — Hover One, Blur the Rest to Draw Attention

Focus cards are the grid interaction where hovering any one card sharpens and lifts it while every other card blurs and dims — pulling all attention to the one you're pointing at. It's a striking way to present a gallery, a team, or a set of features. This snippet builds it with plain HTML and CSS for the core effect, plus a little vanilla JavaScript to make it work on touch and keyboard.
The "dim the siblings" trick
The clever part is achieved entirely with CSS selector scoping. When the grid container is hovered (.fc-grid:hover), a rule blurs, darkens, and slightly shrinks every .fc-card inside it. A second, more specific rule then targets only the card actually under the cursor (.fc-card:hover) and restores it to full sharpness, scales it up, and raises its z-index and shadow. Because the hovered-card rule wins on specificity over the all-cards rule, the net effect is: everything dims except the one you're on. No JavaScript decides which card is active — the cascade does.
Captions that reveal on focus
Each card hides its title and subtitle by default at opacity: 0 and a slight downward offset. Only the focused card's .fc-meta transitions up and in, so the label appears precisely on the card you're attending to. This keeps the grid clean at rest — just colorful tiles — and surfaces information contextually, which is both elegant and reduces visual noise.
Keyboard accessibility with focus-within
The same effect is wired to keyboard navigation. Every card has tabindex="0", and the dimming rules also trigger on .fc-grid:focus-within, with the active styles applying on :focus-visible. So tabbing through the cards produces the identical blur-others-highlight-one behavior, and the caption reveals on the focused card. This means the interaction isn't mouse-only — it's fully operable from the keyboard, with the focus ring replaced by the lift-and-sharpen treatment.
Making it work on touch
Touch devices have no hover, so the JavaScript bridges the gap: tapping a card calls focus() on it, which triggers the :focus-within and :focus-visible rules and produces the same effect as hovering. Tapping empty grid space or pressing Escape blurs the active element to reset. Enter and Space also focus a card for keyboard activation. This small script is what makes the pattern usable on phones, where the pure-CSS hover version would otherwise do nothing.
Theming with a CSS variable
Each card's background is set through a --bg custom property inline, so the six gradients are data, not separate rules. Swap these for real images by setting --bg to a url() or replacing the background with an <img> — the focus logic is independent of the card's content.
Customizing it
Tune the blur(2px) and brightness(.6) for a stronger or subtler dimming, adjust the scale values for more or less lift, change the grid columns, or speed up the .4s transitions. The grid drops from three to two columns under 560px. Pair it with a pin card feature or a testimonial wall for a rich, interactive page.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA grid of six colorful cards renders evenly.
- 2Hover any cardIt sharpens and lifts while the others blur and dim.
- 3See the captionThe focused card reveals its title and subtitle.
- 4Tab through themKeyboard focus produces the same blur-others effect.
- 5Tap on mobileTapping a card focuses it; tap empty space to reset.
- 6Swap in imagesSet each card's --bg to a real image.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
It's pure CSS specificity. A rule on .fc-grid:hover .fc-card blurs and dims every card when the grid is hovered. A more specific rule on .fc-card:hover then restores the single card under the cursor to full sharpness and scales it up. Because the hovered-card rule outranks the all-cards rule, everything dims except the one you point at — no JavaScript picks the active card.
Yes. Every card has tabindex=0, and the dimming rules also fire on .fc-grid:focus-within with the active styles on :focus-visible. Tabbing through the cards produces the same blur-others, highlight-one behavior and reveals the focused card's caption, so the interaction is fully keyboard-operable rather than mouse-only.
The JavaScript bridges that gap. Tapping a card calls focus() on it, which triggers the focus-within and focus-visible rules and reproduces the hover effect. Tapping empty grid space or pressing Escape blurs the active element to reset, and Enter or Space also focuses a card. Without this, the CSS-only hover version would do nothing on phones.
Yes. Each card's background is set via a --bg custom property inline, so the gradients are just data. Replace --bg with a url() image, or swap the background for an <img> element inside the card. The focus, blur, and caption logic is independent of what fills the card.
Render the cards from an array with their background and labels. The hover and focus-within CSS works unchanged. Add an onClick that focuses the card element via a ref for touch, plus an Escape handler in a mount effect with cleanup. In Tailwind, use group and peer or the has-[] and focus-within variants to express the dim-siblings behavior, with blur and scale utilities.
Focus Cards — Export as HTML, React, Vue, Angular & Tailwind
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Focus Cards</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0a0a14;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px}
.fc-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;width:100%;max-width:760px}
.fc-card{position:relative;aspect-ratio:3/4;border-radius:16px;background:var(--bg);overflow:hidden;cursor:pointer;outline:none;transition:transform .4s ease,filter .4s ease,opacity .4s ease;display:flex;align-items:flex-end;padding:18px}
.fc-card::after{content:'';position:absolute;inset:0;background:rgba(10,10,20,.0);transition:background .4s ease}
/* When the grid is hovered, every card dims and blurs EXCEPT the hovered one,
which lifts. The same applies on keyboard focus-within. */
.fc-grid:hover .fc-card,.fc-grid:focus-within .fc-card{filter:blur(2px) brightness(.6);transform:scale(.97);opacity:.8}
.fc-grid .fc-card:hover,.fc-grid .fc-card:focus-visible{filter:none;transform:scale(1.03);opacity:1;z-index:1;box-shadow:0 24px 50px -20px rgba(0,0,0,.7)}
.fc-meta{position:relative;z-index:1;transform:translateY(8px);opacity:0;transition:transform .4s ease,opacity .4s ease}
.fc-card:hover .fc-meta,.fc-card:focus-visible .fc-meta{transform:none;opacity:1}
.fc-meta h3{font-size:19px;font-weight:800}
.fc-meta p{font-size:12.5px;color:rgba(255,255,255,.75);margin-top:2px}
@media(max-width:560px){.fc-grid{grid-template-columns:repeat(2,1fr)}}
</style>
</head>
<body>
<div class="fc-grid" id="fcGrid">
<article class="fc-card" tabindex="0" style="--bg:linear-gradient(150deg,#6366f1,#0a0a14)"><div class="fc-meta"><h3>Aurora</h3><p>Northern lights</p></div></article>
<article class="fc-card" tabindex="0" style="--bg:linear-gradient(150deg,#ec4899,#0a0a14)"><div class="fc-meta"><h3>Ember</h3><p>Desert dusk</p></div></article>
<article class="fc-card" tabindex="0" style="--bg:linear-gradient(150deg,#22d3ee,#0a0a14)"><div class="fc-meta"><h3>Tide</h3><p>Coastal calm</p></div></article>
<article class="fc-card" tabindex="0" style="--bg:linear-gradient(150deg,#34d399,#0a0a14)"><div class="fc-meta"><h3>Fern</h3><p>Forest floor</p></div></article>
<article class="fc-card" tabindex="0" style="--bg:linear-gradient(150deg,#f59e0b,#0a0a14)"><div class="fc-meta"><h3>Solstice</h3><p>Golden hour</p></div></article>
<article class="fc-card" tabindex="0" style="--bg:linear-gradient(150deg,#a78bfa,#0a0a14)"><div class="fc-meta"><h3>Nebula</h3><p>Deep space</p></div></article>
</div>
<script>
// The dim-others / focus-one behaviour is pure CSS via :hover and :focus-within.
// JS only adds a click selection state so the pattern works on touch, where
// there is no hover — tapping a card focuses it and reveals its caption.
var grid = document.getElementById('fcGrid');
var cards = Array.prototype.slice.call(grid.querySelectorAll('.fc-card'));
cards.forEach(function (card) {
card.addEventListener('click', function () {
card.focus();
});
card.addEventListener('keydown', function (e) {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); card.focus(); }
});
});
// Tapping empty grid space (or pressing Escape) clears the focus on touch.
grid.addEventListener('click', function (e) {
if (e.target === grid && document.activeElement) document.activeElement.blur();
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && document.activeElement) document.activeElement.blur();
});
</script>
</body>
</html><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Focus Cards</title>
<!-- Tailwind CSS v3+ — arbitrary value classes (e.g. bg-[#0f172a]) are valid -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
* {
box-sizing:border-box;margin:0;padding:0
}
body {
font-family:system-ui,-apple-system,sans-serif;background:#0a0a14;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px
}
.fc-card::after {
content:'';position:absolute;inset:0;background:rgba(10,10,20,.0);transition:background .4s ease
}
.fc-grid:hover .fc-card,.fc-grid:focus-within .fc-card {
filter:blur(2px) brightness(.6);transform:scale(.97);opacity:.8
}
.fc-grid .fc-card:hover,.fc-grid .fc-card:focus-visible {
filter:none;transform:scale(1.03);opacity:1;z-index:1;box-shadow:0 24px 50px -20px rgba(0,0,0,.7)
}
.fc-card:hover .fc-meta,.fc-card:focus-visible .fc-meta {
transform:none;opacity:1
}
.fc-meta h3 {
font-size:19px;font-weight:800
}
.fc-meta p {
font-size:12.5px;color:rgba(255,255,255,.75);margin-top:2px
}
</style>
</head>
<body>
<div class="fc-grid grid grid-cols-3 gap-4 w-full max-w-[760px] max-[560px]:grid-cols-2" id="fcGrid">
<article class="fc-card relative aspect-[3/4] rounded-2xl [background:var(--bg)] overflow-hidden cursor-pointer outline-none [transition:transform_.4s_ease,filter_.4s_ease,opacity_.4s_ease] flex items-end p-[18px]" tabindex="0" style="--bg:linear-gradient(150deg,#6366f1,#0a0a14)"><div class="fc-meta relative z-[1] [transform:translateY(8px)] opacity-0 [transition:transform_.4s_ease,opacity_.4s_ease]"><h3>Aurora</h3><p>Northern lights</p></div></article>
<article class="fc-card relative aspect-[3/4] rounded-2xl [background:var(--bg)] overflow-hidden cursor-pointer outline-none [transition:transform_.4s_ease,filter_.4s_ease,opacity_.4s_ease] flex items-end p-[18px]" tabindex="0" style="--bg:linear-gradient(150deg,#ec4899,#0a0a14)"><div class="fc-meta relative z-[1] [transform:translateY(8px)] opacity-0 [transition:transform_.4s_ease,opacity_.4s_ease]"><h3>Ember</h3><p>Desert dusk</p></div></article>
<article class="fc-card relative aspect-[3/4] rounded-2xl [background:var(--bg)] overflow-hidden cursor-pointer outline-none [transition:transform_.4s_ease,filter_.4s_ease,opacity_.4s_ease] flex items-end p-[18px]" tabindex="0" style="--bg:linear-gradient(150deg,#22d3ee,#0a0a14)"><div class="fc-meta relative z-[1] [transform:translateY(8px)] opacity-0 [transition:transform_.4s_ease,opacity_.4s_ease]"><h3>Tide</h3><p>Coastal calm</p></div></article>
<article class="fc-card relative aspect-[3/4] rounded-2xl [background:var(--bg)] overflow-hidden cursor-pointer outline-none [transition:transform_.4s_ease,filter_.4s_ease,opacity_.4s_ease] flex items-end p-[18px]" tabindex="0" style="--bg:linear-gradient(150deg,#34d399,#0a0a14)"><div class="fc-meta relative z-[1] [transform:translateY(8px)] opacity-0 [transition:transform_.4s_ease,opacity_.4s_ease]"><h3>Fern</h3><p>Forest floor</p></div></article>
<article class="fc-card relative aspect-[3/4] rounded-2xl [background:var(--bg)] overflow-hidden cursor-pointer outline-none [transition:transform_.4s_ease,filter_.4s_ease,opacity_.4s_ease] flex items-end p-[18px]" tabindex="0" style="--bg:linear-gradient(150deg,#f59e0b,#0a0a14)"><div class="fc-meta relative z-[1] [transform:translateY(8px)] opacity-0 [transition:transform_.4s_ease,opacity_.4s_ease]"><h3>Solstice</h3><p>Golden hour</p></div></article>
<article class="fc-card relative aspect-[3/4] rounded-2xl [background:var(--bg)] overflow-hidden cursor-pointer outline-none [transition:transform_.4s_ease,filter_.4s_ease,opacity_.4s_ease] flex items-end p-[18px]" tabindex="0" style="--bg:linear-gradient(150deg,#a78bfa,#0a0a14)"><div class="fc-meta relative z-[1] [transform:translateY(8px)] opacity-0 [transition:transform_.4s_ease,opacity_.4s_ease]"><h3>Nebula</h3><p>Deep space</p></div></article>
</div>
<script>
// The dim-others / focus-one behaviour is pure CSS via :hover and :focus-within.
// JS only adds a click selection state so the pattern works on touch, where
// there is no hover — tapping a card focuses it and reveals its caption.
var grid = document.getElementById('fcGrid');
var cards = Array.prototype.slice.call(grid.querySelectorAll('.fc-card'));
cards.forEach(function (card) {
card.addEventListener('click', function () {
card.focus();
});
card.addEventListener('keydown', function (e) {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); card.focus(); }
});
});
// Tapping empty grid space (or pressing Escape) clears the focus on touch.
grid.addEventListener('click', function (e) {
if (e.target === grid && document.activeElement) document.activeElement.blur();
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && document.activeElement) document.activeElement.blur();
});
</script>
</body>
</html>import React, { useEffect } from 'react';
// CSS — optionally move to FocusCards.module.css
const css = `
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0a0a14;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px}
.fc-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;width:100%;max-width:760px}
.fc-card{position:relative;aspect-ratio:3/4;border-radius:16px;background:var(--bg);overflow:hidden;cursor:pointer;outline:none;transition:transform .4s ease,filter .4s ease,opacity .4s ease;display:flex;align-items:flex-end;padding:18px}
.fc-card::after{content:'';position:absolute;inset:0;background:rgba(10,10,20,.0);transition:background .4s ease}
/* When the grid is hovered, every card dims and blurs EXCEPT the hovered one,
which lifts. The same applies on keyboard focus-within. */
.fc-grid:hover .fc-card,.fc-grid:focus-within .fc-card{filter:blur(2px) brightness(.6);transform:scale(.97);opacity:.8}
.fc-grid .fc-card:hover,.fc-grid .fc-card:focus-visible{filter:none;transform:scale(1.03);opacity:1;z-index:1;box-shadow:0 24px 50px -20px rgba(0,0,0,.7)}
.fc-meta{position:relative;z-index:1;transform:translateY(8px);opacity:0;transition:transform .4s ease,opacity .4s ease}
.fc-card:hover .fc-meta,.fc-card:focus-visible .fc-meta{transform:none;opacity:1}
.fc-meta h3{font-size:19px;font-weight:800}
.fc-meta p{font-size:12.5px;color:rgba(255,255,255,.75);margin-top:2px}
@media(max-width:560px){.fc-grid{grid-template-columns:repeat(2,1fr)}}
`;
export default function FocusCards() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
// The dim-others / focus-one behaviour is pure CSS via :hover and :focus-within.
// JS only adds a click selection state so the pattern works on touch, where
// there is no hover — tapping a card focuses it and reveals its caption.
var grid = document.getElementById('fcGrid');
var cards = Array.prototype.slice.call(grid.querySelectorAll('.fc-card'));
cards.forEach(function (card) {
card.addEventListener('click', function () {
card.focus();
});
card.addEventListener('keydown', function (e) {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); card.focus(); }
});
});
// Tapping empty grid space (or pressing Escape) clears the focus on touch.
grid.addEventListener('click', function (e) {
if (e.target === grid && document.activeElement) document.activeElement.blur();
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && document.activeElement) document.activeElement.blur();
});
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
}, []);
return (
<>
<style>{css}</style>
<div className="fc-grid" id="fcGrid">
<article className="fc-card" tabindex="0" style={{ '--bg': 'linear-gradient(150deg,#6366f1,#0a0a14)' }}><div className="fc-meta"><h3>Aurora</h3><p>Northern lights</p></div></article>
<article className="fc-card" tabindex="0" style={{ '--bg': 'linear-gradient(150deg,#ec4899,#0a0a14)' }}><div className="fc-meta"><h3>Ember</h3><p>Desert dusk</p></div></article>
<article className="fc-card" tabindex="0" style={{ '--bg': 'linear-gradient(150deg,#22d3ee,#0a0a14)' }}><div className="fc-meta"><h3>Tide</h3><p>Coastal calm</p></div></article>
<article className="fc-card" tabindex="0" style={{ '--bg': 'linear-gradient(150deg,#34d399,#0a0a14)' }}><div className="fc-meta"><h3>Fern</h3><p>Forest floor</p></div></article>
<article className="fc-card" tabindex="0" style={{ '--bg': 'linear-gradient(150deg,#f59e0b,#0a0a14)' }}><div className="fc-meta"><h3>Solstice</h3><p>Golden hour</p></div></article>
<article className="fc-card" tabindex="0" style={{ '--bg': 'linear-gradient(150deg,#a78bfa,#0a0a14)' }}><div className="fc-meta"><h3>Nebula</h3><p>Deep space</p></div></article>
</div>
</>
);
}import React, { useEffect } from 'react';
// Requires Tailwind CSS v3+ — https://tailwindcss.com/docs/installation
// Arbitrary value classes (e.g. bg-[#0f172a]) are valid Tailwind v3+
export default function FocusCards() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
// The dim-others / focus-one behaviour is pure CSS via :hover and :focus-within.
// JS only adds a click selection state so the pattern works on touch, where
// there is no hover — tapping a card focuses it and reveals its caption.
var grid = document.getElementById('fcGrid');
var cards = Array.prototype.slice.call(grid.querySelectorAll('.fc-card'));
cards.forEach(function (card) {
card.addEventListener('click', function () {
card.focus();
});
card.addEventListener('keydown', function (e) {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); card.focus(); }
});
});
// Tapping empty grid space (or pressing Escape) clears the focus on touch.
grid.addEventListener('click', function (e) {
if (e.target === grid && document.activeElement) document.activeElement.blur();
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && document.activeElement) document.activeElement.blur();
});
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
}, []);
return (
<>
<style>{`
* {
box-sizing:border-box;margin:0;padding:0
}
body {
font-family:system-ui,-apple-system,sans-serif;background:#0a0a14;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px
}
.fc-card::after {
content:'';position:absolute;inset:0;background:rgba(10,10,20,.0);transition:background .4s ease
}
.fc-grid:hover .fc-card,.fc-grid:focus-within .fc-card {
filter:blur(2px) brightness(.6);transform:scale(.97);opacity:.8
}
.fc-grid .fc-card:hover,.fc-grid .fc-card:focus-visible {
filter:none;transform:scale(1.03);opacity:1;z-index:1;box-shadow:0 24px 50px -20px rgba(0,0,0,.7)
}
.fc-card:hover .fc-meta,.fc-card:focus-visible .fc-meta {
transform:none;opacity:1
}
.fc-meta h3 {
font-size:19px;font-weight:800
}
.fc-meta p {
font-size:12.5px;color:rgba(255,255,255,.75);margin-top:2px
}
`}</style>
<div className="fc-grid grid grid-cols-3 gap-4 w-full max-w-[760px] max-[560px]:grid-cols-2" id="fcGrid">
<article className="fc-card relative aspect-[3/4] rounded-2xl [background:var(--bg)] overflow-hidden cursor-pointer outline-none [transition:transform_.4s_ease,filter_.4s_ease,opacity_.4s_ease] flex items-end p-[18px]" tabindex="0" style={{ '--bg': 'linear-gradient(150deg,#6366f1,#0a0a14)' }}><div className="fc-meta relative z-[1] [transform:translateY(8px)] opacity-0 [transition:transform_.4s_ease,opacity_.4s_ease]"><h3>Aurora</h3><p>Northern lights</p></div></article>
<article className="fc-card relative aspect-[3/4] rounded-2xl [background:var(--bg)] overflow-hidden cursor-pointer outline-none [transition:transform_.4s_ease,filter_.4s_ease,opacity_.4s_ease] flex items-end p-[18px]" tabindex="0" style={{ '--bg': 'linear-gradient(150deg,#ec4899,#0a0a14)' }}><div className="fc-meta relative z-[1] [transform:translateY(8px)] opacity-0 [transition:transform_.4s_ease,opacity_.4s_ease]"><h3>Ember</h3><p>Desert dusk</p></div></article>
<article className="fc-card relative aspect-[3/4] rounded-2xl [background:var(--bg)] overflow-hidden cursor-pointer outline-none [transition:transform_.4s_ease,filter_.4s_ease,opacity_.4s_ease] flex items-end p-[18px]" tabindex="0" style={{ '--bg': 'linear-gradient(150deg,#22d3ee,#0a0a14)' }}><div className="fc-meta relative z-[1] [transform:translateY(8px)] opacity-0 [transition:transform_.4s_ease,opacity_.4s_ease]"><h3>Tide</h3><p>Coastal calm</p></div></article>
<article className="fc-card relative aspect-[3/4] rounded-2xl [background:var(--bg)] overflow-hidden cursor-pointer outline-none [transition:transform_.4s_ease,filter_.4s_ease,opacity_.4s_ease] flex items-end p-[18px]" tabindex="0" style={{ '--bg': 'linear-gradient(150deg,#34d399,#0a0a14)' }}><div className="fc-meta relative z-[1] [transform:translateY(8px)] opacity-0 [transition:transform_.4s_ease,opacity_.4s_ease]"><h3>Fern</h3><p>Forest floor</p></div></article>
<article className="fc-card relative aspect-[3/4] rounded-2xl [background:var(--bg)] overflow-hidden cursor-pointer outline-none [transition:transform_.4s_ease,filter_.4s_ease,opacity_.4s_ease] flex items-end p-[18px]" tabindex="0" style={{ '--bg': 'linear-gradient(150deg,#f59e0b,#0a0a14)' }}><div className="fc-meta relative z-[1] [transform:translateY(8px)] opacity-0 [transition:transform_.4s_ease,opacity_.4s_ease]"><h3>Solstice</h3><p>Golden hour</p></div></article>
<article className="fc-card relative aspect-[3/4] rounded-2xl [background:var(--bg)] overflow-hidden cursor-pointer outline-none [transition:transform_.4s_ease,filter_.4s_ease,opacity_.4s_ease] flex items-end p-[18px]" tabindex="0" style={{ '--bg': 'linear-gradient(150deg,#a78bfa,#0a0a14)' }}><div className="fc-meta relative z-[1] [transform:translateY(8px)] opacity-0 [transition:transform_.4s_ease,opacity_.4s_ease]"><h3>Nebula</h3><p>Deep space</p></div></article>
</div>
</>
);
}<template>
<div class="fc-grid" id="fcGrid">
<article class="fc-card" tabindex="0" style="--bg:linear-gradient(150deg,#6366f1,#0a0a14)"><div class="fc-meta"><h3>Aurora</h3><p>Northern lights</p></div></article>
<article class="fc-card" tabindex="0" style="--bg:linear-gradient(150deg,#ec4899,#0a0a14)"><div class="fc-meta"><h3>Ember</h3><p>Desert dusk</p></div></article>
<article class="fc-card" tabindex="0" style="--bg:linear-gradient(150deg,#22d3ee,#0a0a14)"><div class="fc-meta"><h3>Tide</h3><p>Coastal calm</p></div></article>
<article class="fc-card" tabindex="0" style="--bg:linear-gradient(150deg,#34d399,#0a0a14)"><div class="fc-meta"><h3>Fern</h3><p>Forest floor</p></div></article>
<article class="fc-card" tabindex="0" style="--bg:linear-gradient(150deg,#f59e0b,#0a0a14)"><div class="fc-meta"><h3>Solstice</h3><p>Golden hour</p></div></article>
<article class="fc-card" tabindex="0" style="--bg:linear-gradient(150deg,#a78bfa,#0a0a14)"><div class="fc-meta"><h3>Nebula</h3><p>Deep space</p></div></article>
</div>
</template>
<script setup>
import { onMounted } from 'vue';
let grid;
let cards;
onMounted(() => {
grid = document.getElementById('fcGrid');
cards = Array.prototype.slice.call(grid.querySelectorAll('.fc-card'));
cards.forEach(function (card) {
card.addEventListener('click', function () {
card.focus();
});
card.addEventListener('keydown', function (e) {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); card.focus(); }
});
});
// Tapping empty grid space (or pressing Escape) clears the focus on touch.
grid.addEventListener('click', function (e) {
if (e.target === grid && document.activeElement) document.activeElement.blur();
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && document.activeElement) document.activeElement.blur();
});
});
</script>
<style scoped>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0a0a14;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px}
.fc-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;width:100%;max-width:760px}
.fc-card{position:relative;aspect-ratio:3/4;border-radius:16px;background:var(--bg);overflow:hidden;cursor:pointer;outline:none;transition:transform .4s ease,filter .4s ease,opacity .4s ease;display:flex;align-items:flex-end;padding:18px}
.fc-card::after{content:'';position:absolute;inset:0;background:rgba(10,10,20,.0);transition:background .4s ease}
/* When the grid is hovered, every card dims and blurs EXCEPT the hovered one,
which lifts. The same applies on keyboard focus-within. */
.fc-grid:hover .fc-card,.fc-grid:focus-within .fc-card{filter:blur(2px) brightness(.6);transform:scale(.97);opacity:.8}
.fc-grid .fc-card:hover,.fc-grid .fc-card:focus-visible{filter:none;transform:scale(1.03);opacity:1;z-index:1;box-shadow:0 24px 50px -20px rgba(0,0,0,.7)}
.fc-meta{position:relative;z-index:1;transform:translateY(8px);opacity:0;transition:transform .4s ease,opacity .4s ease}
.fc-card:hover .fc-meta,.fc-card:focus-visible .fc-meta{transform:none;opacity:1}
.fc-meta h3{font-size:19px;font-weight:800}
.fc-meta p{font-size:12.5px;color:rgba(255,255,255,.75);margin-top:2px}
@media(max-width:560px){.fc-grid{grid-template-columns:repeat(2,1fr)}}
</style>// @ts-nocheck
// Note: vanilla JS DOM manipulation is preserved as-is inside ngAfterViewInit().
// For idiomatic Angular, replace document.getElementById() with @ViewChild() refs
// and move state into component properties with two-way binding.
import { Component, AfterViewInit, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-focus-cards',
standalone: true,
imports: [CommonModule],
encapsulation: ViewEncapsulation.None,
template: `
<div class="fc-grid" id="fcGrid">
<article class="fc-card" tabindex="0" style="--bg:linear-gradient(150deg,#6366f1,#0a0a14)"><div class="fc-meta"><h3>Aurora</h3><p>Northern lights</p></div></article>
<article class="fc-card" tabindex="0" style="--bg:linear-gradient(150deg,#ec4899,#0a0a14)"><div class="fc-meta"><h3>Ember</h3><p>Desert dusk</p></div></article>
<article class="fc-card" tabindex="0" style="--bg:linear-gradient(150deg,#22d3ee,#0a0a14)"><div class="fc-meta"><h3>Tide</h3><p>Coastal calm</p></div></article>
<article class="fc-card" tabindex="0" style="--bg:linear-gradient(150deg,#34d399,#0a0a14)"><div class="fc-meta"><h3>Fern</h3><p>Forest floor</p></div></article>
<article class="fc-card" tabindex="0" style="--bg:linear-gradient(150deg,#f59e0b,#0a0a14)"><div class="fc-meta"><h3>Solstice</h3><p>Golden hour</p></div></article>
<article class="fc-card" tabindex="0" style="--bg:linear-gradient(150deg,#a78bfa,#0a0a14)"><div class="fc-meta"><h3>Nebula</h3><p>Deep space</p></div></article>
</div>
`,
styles: [`
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0a0a14;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px}
.fc-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;width:100%;max-width:760px}
.fc-card{position:relative;aspect-ratio:3/4;border-radius:16px;background:var(--bg);overflow:hidden;cursor:pointer;outline:none;transition:transform .4s ease,filter .4s ease,opacity .4s ease;display:flex;align-items:flex-end;padding:18px}
.fc-card::after{content:'';position:absolute;inset:0;background:rgba(10,10,20,.0);transition:background .4s ease}
/* When the grid is hovered, every card dims and blurs EXCEPT the hovered one,
which lifts. The same applies on keyboard focus-within. */
.fc-grid:hover .fc-card,.fc-grid:focus-within .fc-card{filter:blur(2px) brightness(.6);transform:scale(.97);opacity:.8}
.fc-grid .fc-card:hover,.fc-grid .fc-card:focus-visible{filter:none;transform:scale(1.03);opacity:1;z-index:1;box-shadow:0 24px 50px -20px rgba(0,0,0,.7)}
.fc-meta{position:relative;z-index:1;transform:translateY(8px);opacity:0;transition:transform .4s ease,opacity .4s ease}
.fc-card:hover .fc-meta,.fc-card:focus-visible .fc-meta{transform:none;opacity:1}
.fc-meta h3{font-size:19px;font-weight:800}
.fc-meta p{font-size:12.5px;color:rgba(255,255,255,.75);margin-top:2px}
@media(max-width:560px){.fc-grid{grid-template-columns:repeat(2,1fr)}}
`]
})
export class FocusCardsComponent implements AfterViewInit {
ngAfterViewInit(): void {
// The dim-others / focus-one behaviour is pure CSS via :hover and :focus-within.
// JS only adds a click selection state so the pattern works on touch, where
// there is no hover — tapping a card focuses it and reveals its caption.
var grid = document.getElementById('fcGrid');
var cards = Array.prototype.slice.call(grid.querySelectorAll('.fc-card'));
cards.forEach(function (card) {
card.addEventListener('click', function () {
card.focus();
});
card.addEventListener('keydown', function (e) {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); card.focus(); }
});
});
// Tapping empty grid space (or pressing Escape) clears the focus on touch.
grid.addEventListener('click', function (e) {
if (e.target === grid && document.activeElement) document.activeElement.blur();
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && document.activeElement) document.activeElement.blur();
});
}
}