Spring physics, gyroscope, glare effects, compound layers, keyboard accessibility, and a 60fps performance rewrite — all in one major release.
Introduction
Version 3.0.0 of @gfazioli/mantine-parallax is the biggest update since the package was first published. What started as a simple Apple TV-style tilt card has evolved into a full-featured interactive card system with physically-based animations, mobile device support, and first-class accessibility. Under the hood, the rendering pipeline has been completely rewritten to deliver smooth 60fps updates with zero unnecessary React re-renders.
This release adds 13 new features, fixes 5 bugs, and includes 4 performance/architecture improvements — while staying fully compatible with Mantine 8.x and React 19.
What's New
Parallax.Layer — Compound Component for Multi-Depth Parallax
The new Parallax.Layer compound component replaces the legacy contentParallax + cloneElement approach. Each layer moves independently based on a depth multiplier, giving you precise control over parallax intensity per element.
<Parallax>
<Parallax.Layer depth={1}>
<Text>Background — subtle movement</Text>
</Parallax.Layer>
<Parallax.Layer depth={3}>
<Title>Foreground — dramatic movement</Title>
</Parallax.Layer>
</Parallax>
Layers automatically consume rotation state via React Context — no prop drilling, no cloneElement magic. The legacy contentParallax prop still works but Parallax.Layer is the recommended approach going forward.
Spring Physics Animation
Enable physically-based movement with overshoot and oscillation using a damped harmonic oscillator. The card feels alive — it bounces, settles, and responds organically to cursor movement.
<Parallax
springEffect
springStiffness={150} // higher = snappier
springDamping={12} // higher = less bounce
>
{children}
</Parallax>
Spring parameters can be changed mid-animation — new values take effect immediately with no residual momentum from previous settings.
Glare Reflection Effect
A glare band follows the cursor across the card surface, simulating light reflecting off a glossy surface.
<Parallax
glareEffect
glareColor="rgba(255, 255, 255, 0.4)"
glareMaxOpacity={0.4}
glareSize={30}
glareOverlay
>
{children}
</Parallax>
Dynamic Shadow Effect
A shadow that shifts opposite to the card rotation, creating the illusion of a light source from above. The shadow updates at 60fps during hover and smoothly transitions back to zero on leave.
<Parallax
shadowEffect
shadowColor="rgba(0, 0, 0, 0.4)"
shadowBlur={30}
shadowOffset={0.8}
>
{children}
</Parallax>
Gyroscope Support
On mobile devices, the card tilts based on physical device orientation via the DeviceOrientation API. On iOS 13+, permission is requested on the first user interaction — with built-in guards against duplicate permission prompts.
<Parallax
gyroscopeEnabled
gyroscopeSensitivity={1}
>
{children}
</Parallax>
Keyboard Accessibility
Arrow keys tilt the card, Escape resets it. When enabled, the component adds proper ARIA attributes (tabIndex, role="group", aria-roledescription, aria-label) for screen reader support.
<Parallax
keyboardEnabled
keyboardStep={5} // degrees per key press
>
{children}
</Parallax>
Touch Support
Touch interactions are enabled by default. The card responds to finger movement on mobile, with touch-action: none on the outer container to prevent scroll interference.
<Parallax touchEnabled>{children}</Parallax>
Mantine radius Prop
Standard Mantine radius prop for controlling border radius via the --parallax-radius CSS variable. Supports theme tokens, CSS strings, or pixel numbers.
<Parallax radius="md">...</Parallax>
<Parallax radius={16}>...</Parallax>
Rotation Control Props
Fine-tune how the card responds to interaction:
<Parallax
resetOnLeave={false} // card holds its tilt after mouse leave
invertRotation // card tilts away from cursor
maxRotation={15} // clamp rotation to ±15 degrees
hoverScale={1.05} // subtle scale-up on hover
/>
Transition Customization
Control the transition timing for enter/leave animations:
<Parallax
transitionDuration={300} // ms
transitionEasing="ease-out" // any CSS easing function
/>
onRotationChange Callback
Subscribe to rotation changes for building synchronized UI — debug overlays, companion shadows, linked animations:
<Parallax
onRotationChange={({ rotateX, rotateY, isHovering }) => {
console.log(`Tilt: ${rotateX.toFixed(1)}° × ${rotateY.toFixed(1)}°`);
}}
>
{children}
</Parallax>
prefers-reduced-motion Support
The component automatically respects the OS-level reduced motion setting. All transitions, hover effects, spring animations, and touch/gyroscope interactions are disabled when active — no extra configuration needed.
Performance Rewrite
The internal architecture was rewritten from the ground up:
Native events + requestAnimationFrame: Replaced the
useMousehook (which triggered a React re-render on every pixel of mouse movement) with nativeonMouseMove/onTouchMoveevents throttled viarequestAnimationFrame. Rotation updates are batched at 60fps with zero React re-renders during interaction.GPU-friendly shadow updates:
box-shadowCSS transitions — which are not GPU-accelerated and cause expensive repaints — are no longer applied during active hover. The shadow updates directly via RAF at 60fps. The CSS transition is only used on mouse leave for a smooth fade-out.Mid-hover deactivation: If
disabledorprefers-reduced-motionchanges totruewhile the card is being hovered, the component now properly deactivates instead of continuing to process events.
Bug Fixes
- Light effect persistence: The light gradient overlay no longer disappears from the DOM after mouse leave, preventing a flash when re-entering the card.
-
Background position conflict: Setting
backgroundPositionwithout abackgroundImageno longer causes CSS shorthand/longhand conflicts with Mantine'sbgprop in React 19. - Shadow during spring return: The dynamic shadow now follows the card rotation during spring return animation instead of snapping to zero instantly.
-
Spring parameter hot-swap: Changing
springStiffnessorspringDampingmid-animation uses the new values immediately and resets accumulated velocity. -
Gyroscope permission dedup: Rapid taps no longer trigger multiple permission prompts or register duplicate
deviceorientationlisteners.
Breaking Changes
1. New glare style selector
ParallaxStylesNames now includes 'glare' as a fourth selector. If you have exhaustive classNames or styles overrides:
// Before
<Parallax classNames={{ root: '...', content: '...', light: '...' }} />
// After — add the glare key
<Parallax classNames={{ root: '...', content: '...', light: '...', glare: '...' }} />
2. New --parallax-radius CSS variable
The root element's border-radius is now controlled by a CSS variable. If you were overriding border-radius via styles, the CSS variable takes precedence — use the radius prop instead:
// Before
<Parallax style={{ borderRadius: 16 }}>
// After
<Parallax radius={16}>
3. New exports and factory type changes
ParallaxFactory now includes vars (ParallaxCssVariables) and staticComponents ({ Layer: typeof ParallaxLayer }). New exports: ParallaxLayer, ParallaxLayerProps, ParallaxProvider, useParallaxContext, ParallaxContextValue, ParallaxCssVariables.
Migration from contentParallax to Parallax.Layer
The legacy contentParallax prop still works, but Parallax.Layer is the recommended approach:
// Before (legacy)
<Parallax contentParallax contentParallaxDistance={2}>
<div>Child 1</div>
<div>Child 2</div>
</Parallax>
// After (recommended)
<Parallax>
<Parallax.Layer depth={2}>
<div>Child 1</div>
</Parallax.Layer>
<Parallax.Layer depth={4}>
<div>Child 2</div>
</Parallax.Layer>
</Parallax>
Parallax.Layer gives you explicit control over each element's depth and works with any children — no cloneElement limitations.
New Props at a Glance
| Prop | Type | Default | Description |
|---|---|---|---|
springEffect |
boolean |
false |
Enable spring physics animation |
springStiffness |
number |
150 |
Spring snappiness |
springDamping |
number |
12 |
Spring bounce reduction |
gyroscopeEnabled |
boolean |
false |
Enable device orientation tilt |
gyroscopeSensitivity |
number |
1 |
Gyroscope rotation multiplier |
keyboardEnabled |
boolean |
false |
Enable arrow key interaction |
keyboardStep |
number |
5 |
Degrees per key press |
shadowEffect |
boolean |
false |
Enable dynamic shadow |
shadowColor |
MantineColor |
'rgba(0,0,0,0.4)' |
Shadow color |
shadowBlur |
number |
30 |
Shadow blur radius |
shadowOffset |
number |
0.8 |
Shadow offset multiplier |
glareEffect |
boolean |
false |
Enable glare reflection |
glareColor |
MantineColor |
'rgba(255,255,255,0.4)' |
Glare color |
glareMaxOpacity |
number |
0.4 |
Maximum glare opacity |
glareSize |
number |
30 |
Glare band size |
glareOverlay |
boolean |
true |
Render glare above content |
radius |
MantineRadius |
— | Border radius (theme tokens or px) |
touchEnabled |
boolean |
true |
Enable touch interactions |
hoverScale |
number |
1 |
Scale factor on hover |
resetOnLeave |
boolean |
true |
Reset rotation on leave |
invertRotation |
boolean |
false |
Tilt away from cursor |
maxRotation |
number |
— | Clamp rotation degrees |
transitionDuration |
number |
300 |
Transition time in ms |
transitionEasing |
string |
'ease-out' |
CSS easing function |
onRotationChange |
function |
— | Rotation change callback |
Getting Started
# Install or update
npm install @gfazioli/mantine-parallax@3
# or
yarn add @gfazioli/mantine-parallax@3
Import the styles in your app entry point:
import '@gfazioli/mantine-parallax/styles.css';
Basic usage with the new features:
import { Parallax } from '@gfazioli/mantine-parallax';
function InteractiveCard() {
return (
<Parallax
radius="md"
springEffect
glareEffect
shadowEffect
p={24}
bg="dark.7"
>
<Parallax.Layer depth={1}>
<Text c="dimmed">Background layer</Text>
</Parallax.Layer>
<Parallax.Layer depth={3}>
<Title c="white">Foreground layer</Title>
</Parallax.Layer>
</Parallax>
);
}
Top comments (0)