Skip to content
Brok UI

Loading…

No results

Tilt Card

Open source

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.

Version
v1.0.4
Stability
stable
License
MIT
Related
Card
Holo Card
Scroll Split Card

Preview

Interactive

Tilt Card

Move your pointer across the card to tilt it in 3D and slide the spotlight.

previews.components.tilt-card.default.blade.php 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>

Installation

terminal
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:

resources/js/ui/index.js JS
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.

  • blade resources/views/components/ui/tilt-card.blade.php
  • js resources/js/ui/tilt-card.js
Registry dependencies
None — installs on its own.
Packages
composer: jml/brok:^0.2
npm: 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.

tilt-card.md
# 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

manifest knowledge + registry-derived coverage

Props

Props accepted by this component: name, type, default value and description.
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.

tilt-card

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

Expressive and showcase effect

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
root
Theming hooks
tilt-card

Accessibility

WCAG 2.2 AA Keyboard focus-visible RTL-ready Localized labels Dark mode
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-slot attribute for styling and scripting hooks.
  • Focus-visible rings use the ring token, 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 under dir="rtl" — flip the preview to RTL to confirm.
  • Dark mode uses the same semantic tokens under the dark class; high contrast follows forced-color system tokens.

Livewire

Needs wire:key

Add a stable wire:key when Livewire can reorder this interactive component.

livewire-component.blade.php Blade
<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.

resources/views/components/ui/tilt-card.blade.php Blade
@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>
resources/js/ui/tilt-card.js JS
/**
 * 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