Tilt Card
A generic interactive 3D-tilt wrapper for arbitrary content: on pointer move the card rotates in perspective toward (or away from) the cursor, scales up slightly, and shows a soft token-coloured spotlight glare that follows the pointer. Tilt is skipped under prefers-reduced-motion; colours derive from semantic tokens only.
Preview
Interactive
Tilt Card
Move your pointer across the card to tilt it in 3D and slide the spotlight.
<div class="flex h-64 w-full max-w-md items-center justify-center">
<x-ui.tilt-card class="w-64">
<div class="flex flex-col gap-2">
<p class="text-xs font-medium uppercase tracking-wide text-muted-foreground">Interactive</p>
<p class="text-lg font-semibold text-foreground">Tilt Card</p>
<p class="text-sm text-muted-foreground">Move your pointer across the card to tilt it in 3D and slide the spotlight.</p>
</div>
</x-ui.tilt-card>
</div>
Installation
php artisan ui:add tilt-card
Note
This component ships an Alpine behavior module at
resources/js/ui/tilt-card.js. Import it once from your bundle so it registers on alpine:init:
import './tilt-card.js';
Registry contract
php artisan ui:add tilt-card
writes only the files below. The CLI validates each file hash before writing and asks before it replaces a local change, unless you pass --force.
-
resources/views/components/ui/tilt-card.blade.php -
resources/js/ui/tilt-card.js
- Registry dependencies
- None — installs on its own.
- Packages
-
composer: jml/brok:^0.2npm: alpinejs
Use with AI
A brief for your coding agent: install command, usage, props, guidance and the rules. Copy it, or open a prompt about this component in an assistant.
# Brok UI: Tilt Card (`tilt-card`)
A generic interactive 3D-tilt wrapper for arbitrary content: on pointer move the card rotates in perspective toward (or away from) the cursor, scales up slightly, and shows a soft token-coloured spotlight glare that follows the pointer. Tilt is skipped under prefers-reduced-motion; colours derive from semantic tokens only.
Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.
## Install
```bash
php artisan ui:add tilt-card
```
## Usage
```blade
<div class="flex h-64 w-full max-w-md items-center justify-center">
<x-ui.tilt-card class="w-64">
<div class="flex flex-col gap-2">
<p class="text-xs font-medium uppercase tracking-wide text-muted-foreground">Interactive</p>
<p class="text-lg font-semibold text-foreground">Tilt Card</p>
<p class="text-sm text-muted-foreground">Move your pointer across the card to tilt it in 3D and slide the spotlight.</p>
</div>
</x-ui.tilt-card>
</div>
```
## Props
- `tiltLimit` (int, default `15`) — Maximum rotation, in degrees, applied on either axis.
- `scale` (float, default `1.05`) — Scale factor applied to the surface while the pointer is over it.
- `perspective` (int, default `1200`) — CSS perspective, in pixels, applied to the 3D transform.
- `spotlight` (bool, default `true`) — Adds a pointer-following glare over the surface.
- `effect` (string, default `gravitate`) — Tilt direction: gravitate (toward the pointer) or evade (away from it).
## Use when
- Use only when visual expression supports storytelling, brand differentiation, or a clear showcase moment.
- Wrapping arbitrary card content with a pointer-driven 3D tilt, scale, and spotlight glare effect.
- Choosing whether the tilt gravitates toward the pointer or evades it, for a more playful interaction.
## Avoid when
- Do not place heavy effects in task-dense workflows where clarity, speed, and readability matter more.
- The card needs to stay flat for readability, such as dense data or long-form text; the tilt distorts content as the pointer moves.
## Anti-patterns
- Heavy effects in forms or task-dense workflows
- Motion that interferes with reading or focus
## Rules
- Use the `<brok:tilt-card>` tag (or `<x-ui.tilt-card>`) in Blade; do not rewrite the component.
- Prefer the documented props and variants over utility-class overrides; when a utility must win, use the `!` important modifier.
- Use semantic design tokens (`bg-primary`, `text-muted-foreground`), never raw colour utilities.
- Keep the `data-slot` attributes; they are the styling and test hooks.
## Links
- Docs: https://brokui.dev/docs/components/tilt-card
- Registry JSON (files, props, contract): https://brokui.dev/r/open/tilt-card.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| tiltLimit | int | 15 | Maximum rotation, in degrees, applied on either axis. |
| scale | float | 1.05 | Scale factor applied to the surface while the pointer is over it. |
| perspective | int | 1200 | CSS perspective, in pixels, applied to the 3D transform. |
| spotlight | bool | true | Adds a pointer-following glare over the surface. |
| effect | string | gravitate | Tilt direction: gravitate (toward the pointer) or evade (away from it). |
Slots
default—
Data slots
Stable hooks for CSS overrides and browser tests.
Behavior
- Tracks the pointer over the host to rotate the inner surface toward (gravitate) or away from (evade) the cursor, scaling it up slightly.
- spotlight slides a soft currentColor glare across the surface following the pointer; both the tilt and the spotlight are skipped under prefers-reduced-motion.
- Installs a JavaScript behavior module when the registry item includes resources/js/ui files.
- Declares registry capability flags: a11y, interactive, responsive, rtl, darkMode, localized, reducedMotion.
Guidance
Support brand expression or a showcase moment.
Use when
- Use only when visual expression supports storytelling, brand differentiation, or a clear showcase moment.
- Wrapping arbitrary card content with a pointer-driven 3D tilt, scale, and spotlight glare effect.
- Choosing whether the tilt gravitates toward the pointer or evades it, for a more playful interaction.
Avoid when
- Do not place heavy effects in task-dense workflows where clarity, speed, and readability matter more.
- The card needs to stay flat for readability, such as dense data or long-form text; the tilt distorts content as the pointer moves.
Use instead
- Static content-first presentation
Anti-patterns
- Heavy effects in forms or task-dense workflows
- Motion that interferes with reading or focus
- Anatomy
- Theming hooks
Accessibility
- Keyboard
- managed
- Focus
none
- The spotlight overlay is aria-hidden, since it is a purely visual glare with no informational content.
- Semantic HTML and a stable
data-slotattribute for styling and scripting hooks. - Focus-visible rings use the
ringtoken, so keyboard focus is always visible. - Disabled and invalid states are conveyed to assistive tech, not by color alone.
- Targets WCAG 2.2 AA; verify contrast in light, dark, admin and customer surfaces in the preview.
- Labels go through
__()and layout uses logical properties (ms-*,text-start), so it mirrors underdir="rtl"— flip the preview to RTL to confirm. - Dark mode uses the same semantic tokens under the
darkclass; high contrast follows forced-color system tokens.
Livewire
Add a stable wire:key when Livewire can reorder this interactive component.
<div wire:key="tilt-card-{{ $record->id }}">
<div class="flex h-64 w-full max-w-md items-center justify-center">
<x-ui.tilt-card class="w-64">
<div class="flex flex-col gap-2">
<p class="text-xs font-medium uppercase tracking-wide text-muted-foreground">Interactive</p>
<p class="text-lg font-semibold text-foreground">Tilt Card</p>
<p class="text-sm text-muted-foreground">Move your pointer across the card to tilt it in 3D and slide the spotlight.</p>
</div>
</x-ui.tilt-card>
</div>
</div>
Source
The exact, editable files ui:add writes
into your app. Previews render this same code; there are no preview-only components.
@props([
'tiltLimit' => 15,
'scale' => 1.05,
'perspective' => 1200,
'spotlight' => true,
'effect' => 'gravitate',
])
@php
$config = [
'tiltLimit' => (float) $tiltLimit,
'scale' => (float) $scale,
'effect' => $effect === 'evade' ? 'evade' : 'gravitate',
'spotlight' => (bool) $spotlight,
];
@endphp
{{--
Tilt card — a generic 3D-tilt wrapper you put arbitrary content inside.
The `uiTiltCard` Alpine factory tracks the pointer over the host to rotate
the inner surface in perspective toward (gravitate) or away from (evade) the
cursor, scale it up slightly, and slide a soft token-coloured spotlight glare
across it. All pointer listeners are bound via x-on so Alpine removes them on
teardown (no leaks), and the tilt math is skipped under prefers-reduced-motion.
`perspective` is a dynamic prop, so it is set via an inline style (not a JIT-
invisible arbitrary class). Colours are semantic tokens only — the spotlight
uses `currentColor` from a token text class.
--}}
<div
data-slot="tilt-card"
x-data="uiTiltCard(@js($config))"
x-ref="root"
x-on:pointerenter="enter()"
x-on:pointermove="move($event)"
x-on:pointerleave="leave()"
style="perspective: {{ (int) $perspective }}px"
{{ $attributes->merge(['class' => 'inline-block [transform-style:preserve-3d]']) }}
>
<div
class="relative h-full w-full rounded-lg border border-border bg-card p-6 shadow-lg transition-transform duration-200 ease-out will-change-transform [transform-style:preserve-3d] motion-reduce:transition-none"
x-bind:style="`transform: rotateX(${rx}deg) rotateY(${ry}deg) scale(${scaleNow});`"
>
{{ $slot }}
@if ($spotlight)
<div
aria-hidden="true"
class="pointer-events-none absolute inset-0 rounded-[inherit] text-foreground opacity-0 transition-opacity duration-200 ease-out mix-blend-soft-light bg-[radial-gradient(circle_at_var(--tc-x,50%)_var(--tc-y,50%),currentColor,transparent_60%)]"
x-bind:style="`--tc-x:${gx}%;--tc-y:${gy}%;opacity:${active?0.18:0}`"
></div>
@endif
</div>
</div>
/**
* Tilt card behavior (a generic interactive 3D-tilt wrapper for any content).
*
* The host element listens for pointer enter/move/leave (bound in Blade via
* x-on, so Alpine removes them on teardown — no leaks). On `move()` we map the
* pointer position within the host's bounding box to two rotation values:
* - `ry` from the horizontal offset, `rx` from the vertical offset, each
* scaled by `tiltLimit` degrees;
* - the sign is flipped when `effect === 'evade'` so the card tilts AWAY from
* the cursor instead of toward it.
* `gx`/`gy` are the pointer position as percentages, driving a token-coloured
* spotlight glare in the Blade. `enter()` scales the card up to `scale` and
* reveals the glare; `leave()` resets everything to rest.
*
* Reduced motion: when the user prefers reduced motion the tilt math is skipped
* entirely (rotations stay at 0), so the card never moves.
*
* Self-registers on `alpine:init` so import order does not matter.
*
* Note: this file is plain JS served by the registry — no Blade syntax allowed.
*/
document.addEventListener('alpine:init', () => {
window.Alpine.data('uiTiltCard', (config = {}) => ({
tiltLimit: Number.isFinite(config.tiltLimit) ? config.tiltLimit : 15,
scale: Number.isFinite(config.scale) ? config.scale : 1.05,
effect: config.effect === 'evade' ? 'evade' : 'gravitate',
spotlight: config.spotlight !== false,
rx: 0,
ry: 0,
gx: 50,
gy: 50,
active: false,
scaleNow: 1,
reduced: false,
init() {
this.reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
},
enter() {
this.active = true;
if (!this.reduced) {
this.scaleNow = this.scale;
}
},
move(e) {
if (this.reduced) return;
const r = this.$refs.root.getBoundingClientRect();
if (!r.width || !r.height) return;
const px = (e.clientX - r.left) / r.width;
const py = (e.clientY - r.top) / r.height;
const dir = this.effect === 'evade' ? -1 : 1;
this.ry = (px - 0.5) * 2 * this.tiltLimit * dir;
this.rx = (0.5 - py) * 2 * this.tiltLimit * dir;
this.gx = Math.round(px * 100);
this.gy = Math.round(py * 100);
},
leave() {
this.rx = 0;
this.ry = 0;
this.gx = 50;
this.gy = 50;
this.active = false;
this.scaleNow = 1;
},
destroy() {
this.active = false;
this.scaleNow = 1;
},
}));
});
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.0.4 - Status
- Stable
- License
-
open - Accessibility reviewed
- No review date recorded
- Last breaking change
- No date recorded
- Deprecation
- Not deprecated
- Contract
-
v6 - Foundation
-
≥ 1.0.0