---
title: "Glass Card"
description: "A layered glass surface whose specular highlight, light sweep and chromatic rim track the pointer."
canonical_url: "https://nuxt-motion-block.pages.dev/components/cards/glass-card"
---
# Glass Card

> A layered glass surface whose specular highlight, light sweep and chromatic rim track the pointer.

A glass surface built from four stacked layers: a blurred body, a refracting rim, a
pointer-tracked specular highlight with a vertical light sweep, and a chromatic rim
gradient blended additively over the top. Pointer movement springs all of them toward
the cursor; leaving returns them to rest.

Because `backdrop-filter` only shows what is behind the element, the effect is
invisible on a flat page. Put something with colour and contrast behind it — a
gradient, an image, or blurred blobs — and the panel starts reading as glass.

## Usage

Wrap any content in `MGlassCard` and give it a background to refract. `class` merges
onto the root, so `rounded-*` on your side lines up with the rim, which paints
`rounded-[inherit]` at the radius the component sets.

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



</component-example>
</div>

<template v-slot:code="">

```vue
<template>
  <div class="relative w-full max-w-md overflow-hidden rounded-3xl bg-gradient-to-br from-rose-500 via-violet-500 to-sky-500 p-6">
    <div class="absolute -left-6 top-4 size-24 rounded-full bg-amber-300/80 blur-2xl" />
    <div class="absolute -right-4 bottom-2 size-28 rounded-full bg-teal-300/80 blur-2xl" />

    <MGlassCard class="rounded-3xl">
      <div class="space-y-4 text-white">
        <p class="text-xs font-medium uppercase tracking-widest text-white/70">
          Focus session
        </p>
        <h3 class="text-2xl font-semibold leading-tight">
          Deep work, uninterrupted
        </h3>
        <p class="text-sm text-white/80">
          Move the pointer across the panel — the highlight, the vertical sweep and
          the chromatic rim all follow it, then settle back on leave.
        </p>
        <UButton
          label="Start session"
          icon="i-lucide-play"
        />
      </div>
    </MGlassCard>
  </div>
</template>
```

</template>
</code-preview>

## Customize

<code-preview label="Preview">
<div className="w-full">
<component-example name="glass-card-custom">



</component-example>
</div>

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



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

## Tuning the surface

Each visual layer is a prop, so the surface can be tuned without leaving the
component. The two that move are driven by the pointer; the rest are static.

<table>
<thead>
  <tr>
    <th>
      Prop
    </th>
    
    <th>
      What it changes
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        blur
      </code>
    </td>
    
    <td>
      Body blur, plus saturate and a small brightness lift. The rim's own blur is derived at 12% of this, never less than <code>
        1px
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        rimWidth
      </code>
    </td>
    
    <td>
      Thickness of the refracting ring. Also sets the content padding (<code>
        max(20, rimWidth + 10)
      </code>
      
      ) and the body's inner radius.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        tint
      </code>
    </td>
    
    <td>
      The body's background colour, painted over the blur. <code>
        oklch(1 0 0 / 0.1)
      </code>
      
       by default — mostly white at low alpha.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        refraction
      </code>
    </td>
    
    <td>
      Displacement scale of the rim's turbulence. <code>
        0
      </code>
      
       disables the bend entirely, leaving a blurred rim.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        specular
      </code>
    </td>
    
    <td>
      Whether the highlight and the light sweep render at all. Turning it off leaves a static, still-glass panel.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        border
      </code>
    </td>
    
    <td>
      The inset inner ring that reads as the pane's edge highlight.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        shadow
      </code>
    </td>
    
    <td>
      The drop shadow beneath the card.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        radius
      </code>
    </td>
    
    <td>
      Corner radius of the root, in pixels.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        interactive
      </code>
    </td>
    
    <td>
      The whole pointer-tracking layer.
    </td>
  </tr>
</tbody>
</table>

## Props

<component-props>
<component-prop name="blur" type="number" default="11">

Backdrop blur in pixels applied to the body, together with a `saturate(160%)` and a
`brightness(1.02)` lift. The rim's blur is derived from it at 12%, floored at `1px`,
so the two layers stay in proportion at any value.

</component-prop>

<component-prop name="border" type="boolean" default="true">

Renders the inset inner ring — the two light hairlines plus the soft outer edge —
that reads as the pane's physical thickness. Turn it off for a borderless sheet.

</component-prop>

<component-prop name="interactive" type="boolean" default="true">

Whether the pointer is tracked at all. When false the rAF loop and the
`pointermove`/`pointerleave` listeners are never attached and the panel renders
statically, still glass, no highlight.

</component-prop>

<component-prop name="radius" type="number" default="26">

Corner radius in pixels. The body's own radius is `max(radius - rimWidth, 6)`, so the
rim always sits on a tighter curve than the outer edge.

</component-prop>

<component-prop name="refraction" type="number" default="24">

Scale of the `feDisplacementMap` that bends the rim's backdrop. The effect is also
gated on a runtime capability probe, so this is a ceiling rather than a guarantee —
see the tip.

</component-prop>

<component-prop name="rimWidth" type="number" default="12">

Thickness in pixels of the refracting ring, applied as masked padding. This is the
only prop that changes layout: it also sets the content padding to
`max(20, rimWidth + 10)` so your slot content can never sit under the rim.

</component-prop>

<component-prop name="shadow" type="boolean" default="true">

Applies the two-layer drop shadow beneath the card. When false the root gets no
`box-shadow` at all, so nothing is left behind to tint the glass from below.

</component-prop>

<component-prop name="specular" type="boolean" default="true">

Renders the radial specular highlight and the narrow vertical light sweep that track
the pointer. When false those two layers are not created and only the rim and body
remain.

</component-prop>

<component-prop name="tint" type="string" default="'oklch(1 0 0 / 0.1)'">

Background colour of the body layer, painted over the backdrop blur. Any CSS colour
works; low-alpha white is the default so the panel takes on whatever is behind it.

</component-prop>

<component-prop name="class" type="string">

Merged onto the root, which is already `relative overflow-hidden` with its radius and
shadow set inline. Set your own `rounded-*` to match.

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

## Slots

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

The panel's content. Wrapped in a `relative` div that carries the computed content
padding, so it always clears the rim.

</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.glassCard` 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">

`relative overflow-hidden`

</component-slot-row>

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

`pointer-events-none absolute h-0 w-0`

</component-slot-row>

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

`absolute`

</component-slot-row>

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

`absolute inset-0 rounded-[inherit]`

</component-slot-row>

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

`pointer-events-none absolute inset-0 isolate overflow-hidden rounded-[inherit]`

</component-slot-row>

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

`absolute inset-0 rounded-[inherit]`

</component-slot-row>

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

`absolute`

</component-slot-row>

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

`absolute top-0`

</component-slot-row>

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

`absolute inset-0 rounded-[inherit]`

</component-slot-row>

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

`pointer-events-none absolute inset-0 rounded-[inherit]`

</component-slot-row>

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

`relative`

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

## Accessibility

Every decorative layer — the SVG filter host, the body, the rim, the chromatic
gradient, the specular highlight, the sweep, the bottom shade and the inner ring — is
`aria-hidden="true"`, and the SVG host is `focusable="false"`. The component adds
nothing to the accessibility tree; only your slot content does.

There is no role and no keyboard behaviour: the surface is a presentation wrapper, and
the hover lift is a pointer affordance. Two guards keep it out of the way when it
should not be there — tracking only runs under `(hover: hover) and (pointer: fine)`,
and `useReducedMotion` disables it entirely, removing both the pointer tracking and
the `whileHover` lift and collapsing the root transition to `duration: 0`.

Anything focusable inside the card keeps its own focus ring; nothing here clips or
restyles it.

## Performance

- The pointer loop writes `transform` directly onto three plain `div` layers from a
single `requestAnimationFrame` callback. Motion's own DOM pipeline snapshots
`MotionValue`s handed over through `style` once at mount, so the springs are read
manually instead — Vue does not re-render on any frame.
- `pointermove` and `pointerleave` are attached lazily to the resolved root element,
only when `interactive` is true, and both are removed on unmount along with the rAF
frame and the `matchMedia` listener.
- Three layers composite with `mix-blend-mode: plus-lighter`; only the two that move
get `will-change: transform`.
- `rimWidth` is the one layout-affecting prop: it drives the content padding, so
changing it live reflows the slot content. Everything else is paint or composite.
- The rim's refraction adds an SVG `feTurbulence` + `feDisplacementMap` filter whose id
is namespaced per instance with `useId`. The component probes `CSS.supports` for it
once on mount and skips the filter node entirely where it is unsupported.

<tip>

The refraction is Chromium-only — `backdrop-filter` cannot composite an SVG
displacement map anywhere else. Rather than dropping the whole declaration, the mount
probe swaps `url(#filterId) blur(npx) saturate(…)` for plain `blur(npx) saturate(…)`,
so Safari and Firefox keep a blurred, saturated rim and lose only the bend.

</tip>


## Sitemap

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