---
title: "Glow Hover Cards"
description: "A responsive grid of cards, each with a radial glow that follows the pointer inside it."
canonical_url: "https://nuxt-motion-block.pages.dev/components/cards/glow-hover-cards"
---
# Glow Hover Cards

> A responsive grid of cards, each with a radial glow that follows the pointer inside it.

A responsive grid — one column, two from `sm`, three from `lg` — of `UCard`s 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.

<code-preview label="Preview">
<div className="flex,justify-center,w-full,h-full">
<component-example name="glow-hover-cards-basic">



</component-example>
</div>

<template v-slot:code="">

```vue
<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>
```

</template>
</code-preview>

## Customize

<code-preview label="Preview">
<div className="w-full">
<component-example name="glow-hover-cards-custom">



</component-example>
</div>

<template v-slot:code="">
<playground-code name="GlowHoverCards">



</playground-code>
</template>
</code-preview>

## Props

<component-props>
<component-prop name="items" type="{ 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.

</component-prop>

<component-prop name="glowColor" type="string" default="'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.

</component-prop>

<component-prop name="ui" type="CardProps['ui']" default="undefined">

Forwarded to every `UCard` in the grid, which is the way to change padding or the
border across the whole set at once.

</component-prop>

<component-prop name="class" type="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.

</component-prop>
</component-props>

## Slots

<component-slots>
<component-slot-row name="default" scoped="{ 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.

</component-slot-row>

<component-slot-row name="header" scoped="{ 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.

</component-slot-row>

<component-slot-row name="footer" scoped="{ card, index }">

Forwarded to `UCard`'s `footer` slot when provided, with the same `card` and `index`
props.

</component-slot-row>
</component-slots>

## UI

This component's theme exposes the slots below. Override any of them per instance with the [`ui` prop](https://nuxt-motion-block.pages.dev/getting-started/theming), or app-wide under `ui.glowHoverCards` in `app.config.ts`.

<component-props>
<component-prop name="ui" type="object">

Per-slot classes merged over the theme defaults.

</component-prop>
</component-props>

<component-slots>
<component-slot-row name="root">

`grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6`

</component-slot-row>

<component-slot-row name="card">

`relative overflow-hidden transition-shadow duration-300`

</component-slot-row>

<component-slot-row name="content">

`space-y-2`

</component-slot-row>

<component-slot-row name="title">

`text-lg font-semibold text-highlighted`

</component-slot-row>

<component-slot-row name="description">

`text-sm text-muted`

</component-slot-row>
</component-slots>

## 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 `mousemove` replaces one entry in a plain record of positions and recomputes
that card's `background`. Vue re-renders the card it is over, not the grid — the
other cards' glow positions are preserved across the update.
- `mouseleave` deletes 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-gradient` on the card's own background, and the card
already carries `overflow-hidden`, so it costs no extra DOM and no extra stacking
context.
- `transition-shadow duration-300` is already on every card, which means the class you
pass through `class` should change a shadow or nothing — changing background
gradients will not animate.
- Grid columns are fixed at `sm` and `lg` breakpoints, so the layout reflows at the
viewport and never measures the items.


## Sitemap

See the full [sitemap](https://nuxt-motion-block.pages.dev/sitemap.md) for all pages.
