Glow Hover Cards
A responsive grid — one column, two from sm, three from lg — of UCards that each
track the pointer independently. Moving the cursor inside a card paints a 600px radial
gradient at that point, mixed from glowColor into the card's background; leaving
removes it.
Each slot is scoped with the item it is rendering, so one template can lay out every card in the list.
Usage
Pass items and let the default slot render title and description on its own.
Supply the default slot yourself — along with header or footer — the moment the
cards need anything the item does not carry.
Streaming ingest
Events land in the warehouse in under a second.
Columnar store
Point queries over a trillion rows stay sub-second.
Row policies
One policy engine, enforced per request.
<template>
<div class="w-full max-w-lg">
<MGlowHoverCards
:items="items"
glow-color="var(--ui-primary)"
>
<template #header="{ card }">
<UIcon
:name="card.icon"
class="size-5 text-primary"
/>
</template>
<template #default="{ card }">
<div class="space-y-1">
<h3 class="text-sm font-semibold text-highlighted">
{{ card.title }}
</h3>
<p class="text-sm text-muted">
{{ card.description }}
</p>
</div>
</template>
</MGlowHoverCards>
</div>
</template>
<script setup lang="ts">
const items = [
{
title: 'Streaming ingest',
description: 'Events land in the warehouse in under a second.',
icon: 'i-lucide-zap',
},
{
title: 'Columnar store',
description: 'Point queries over a trillion rows stay sub-second.',
icon: 'i-lucide-database',
},
{
title: 'Row policies',
description: 'One policy engine, enforced per request.',
icon: 'i-lucide-shield-check',
},
]
</script>
Customize
Streaming ingest
Events land in the warehouse in under a second.
Columnar store
Point queries over a trillion rows stay sub-second.
Row policies
One policy engine, enforced per request.
Customize
Props
| Prop | Type | Default | Description |
|---|---|---|---|
items | { title: string, description?: string, [key: string]: any }[] | - | Required. The cards to render, in order. Only title is relied on — description is
used by the default slot — but the type carries an index signature, so extra keys
like icon or href pass through and are available to your slot props. |
glowColor | string | 'var(--ui-primary)' | Any CSS colour. It is interpolated with color-mix(in srgb, … 8%, transparent) and
composited into the radial gradient, so a raw hex, an oklch() value and a theme
token all work, and the alpha is chosen by the component rather than by you. |
ui | CardProps['ui'] | undefined | Forwarded to every UCard in the grid, which is the way to change padding or the
border across the whole set at once. |
class | string | - | Merged onto every card, not just the first. Use it for the per-card hover treatment
— the component already supplies relative overflow-hidden transition-shadow duration-300, so appending is enough. |
Slots
| Slot | Props | Description |
|---|---|---|
default | { card, index } | The card body. Receives the item as card and its position as index. Falls back
to a title heading and an optional description paragraph when the slot is not
provided. |
header | { card, index } | Forwarded to UCard's header slot when provided. Scoped with the same card and
index props; skipped entirely when absent, so cards get no header padding. |
footer | { card, index } | Forwarded to UCard's footer slot when provided, with the same card and index
props. |
UI
This component's theme exposes the slots below. Override any of them per instance with the ui prop, or app-wide under ui.glowHoverCards in app.config.ts.
| Prop | Type | Default | Description |
|---|---|---|---|
ui | object | - | Per-slot classes merged over the theme defaults. |
| Slot | Props | Description |
|---|---|---|
root | - | grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 |
card | - | relative overflow-hidden transition-shadow duration-300 |
content | - | space-y-2 |
title | - | text-lg font-semibold text-highlighted |
description | - | text-sm text-muted |
Accessibility
The component adds no roles, no ARIA attributes and no keyboard behaviour. The glow is
a pointer-only background: mousemove sets it, mouseleave clears it, and nothing
about the card changes for a keyboard or screen-reader user except that the card is a
plain card.
Two things are yours to supply. If a card is meant to be clickable, wrap it in an
anchor or button and put the interaction there — nothing in this component makes a
card focusable. And if a card's meaning depends on the image or icon a sighted user
sees, that needs a text equivalent in your slot; the fallback markup renders title
and description as plain text but nothing is added to it for you.
There is no prefers-reduced-motion handling. The glow is a static gradient at a new
position per move, not an animation, so there is nothing to suppress — but if your page
needs the guarantee, drive glowColor to a neutral value or keep the grid off
pointer devices.
Performance
- Each
mousemovereplaces one entry in a plain record of positions and recomputes that card'sbackground. Vue re-renders the card it is over, not the grid — the other cards' glow positions are preserved across the update. mouseleavedeletes the entry with a rest-spread, so leaving a card restores the remaining positions without mutating the object in place.- The glow is a single
radial-gradienton the card's own background, and the card already carriesoverflow-hidden, so it costs no extra DOM and no extra stacking context. transition-shadow duration-300is already on every card, which means the class you pass throughclassshould change a shadow or nothing — changing background gradients will not animate.- Grid columns are fixed at
smandlgbreakpoints, so the layout reflows at the viewport and never measures the items.