spin-card-react-web
v0.2.0
Published
A composable React card with an organic tilt and cinematic spin transition.
Maintainers
Readme
Spin Card
A composable React card with an organic hover tilt and a cinematic whip-spin transition.
Spin Card owns the interaction. You own everything inside it.
npm · Live playground · GitHub · Created by Polo García
<SpinCard theme={{ background: "#234dde", foreground: "#f8f3e8" }}>
<YourComposition />
</SpinCard>- Arbitrary React children—no prescribed card schema
- Link and button semantics built in
- Image and video backgrounds with custom reverse faces
- Theme-aware media tinting with full manual control
- Pointer-reactive holographic foil for image and video cards
- Configurable tilt, spin, blur, and optional color burst
- Router adapters for client-side navigation
- Reduced-motion and touch-friendly defaults
- TypeScript-first with no runtime dependencies
Playground
Open the interactive playground to try the motion controls, upload your own image or video, and switch between burst and spin-only transitions.
The included Vite demo also runs locally:
npm install
npm run devInstall
npm install spin-card-react-webImport the stylesheet once near the root of your app:
import "spin-card-react-web/styles.css";Quick start
Wrap your cards in SpinTransitionProvider, then pass any composition to SpinCard:
import {
SpinCard,
SpinTransitionProvider,
} from "spin-card-react-web";
import "spin-card-react-web/styles.css";
export function ProjectGallery() {
return (
<SpinTransitionProvider>
<SpinCard
href="/projects/weather"
theme={{
background: "#c7e34a",
foreground: "#17210e",
accent: "#2d57dc",
}}
>
<article className="project-card">
<WeatherMap />
<h2>Weather, composed your way</h2>
</article>
</SpinCard>
</SpinTransitionProvider>
);
}With href, the card renders a native anchor. Without it, the card renders a button and calls onActivate at the transition's activation point:
<SpinCard onActivate={() => setSelected(true)}>
<YourComposition />
</SpinCard>href and onActivate are mutually exclusive: use href for navigation and
onActivate for actions such as opening a modal or updating local state.
The card is itself interactive, so do not nest links or buttons inside it. For compositions with multiple controls, use useSpinTransition.
Your composition, your fonts
SpinCard renders your React tree directly. It does not prescribe a schema, add a content wrapper, or impose padding, typography, or layout.
<SpinCard className="brand-card">
<article className="brand-composition">
<BrandArtwork />
<h2>Your layout and typography</h2>
<Metrics />
</article>
</SpinCard>The shell uses font: inherit and does not load font files. Apply any local, hosted, variable, or design-system font through ordinary CSS:
.brand-card {
font-family: "Your Sans", sans-serif;
}
.brand-composition h2 {
font-family: "Your Display", serif;
font-variation-settings: "wght" 720;
}Font loading remains in your application, so Spin Card adds no font dependency or network request.
Image backgrounds
Use background for an image layer behind your content. Gradients and overlays remain regular CSS:
<SpinCard
href="/stories/coast"
background={{
src: "/images/coast.jpg",
overlay: "linear-gradient(180deg, transparent 35%, rgb(0 0 0 / 72%))",
position: "center",
}}
theme={{ foreground: "#fff" }}
>
<article className="story-card">
<span>Field notes</span>
<h2>The Atlantic coast</h2>
</article>
</SpinCard>Video backgrounds
Use video for a muted, autoplaying background. Include a poster so the card has an immediate fallback while loading and a static presentation when reduced motion is preferred:
<SpinCard
video={{
src: "/video/magic.mp4",
poster: "/images/magic-poster.jpg",
overlay: "linear-gradient(180deg, transparent, rgb(8 6 20 / 78%))",
position: "center",
loop: true,
}}
theme={{ foreground: "#fff" }}
>
<article className="story-card">
<span>Motion study</span>
<h2>A little more magic</h2>
</article>
</SpinCard>Background videos are always muted and play inline. Supported formats depend on the browser; MP4 and WebM cover modern browsers well.
Spin Card sets both the live and default muted states so autoplay works with Safari's media policy. A poster is still recommended for loading, reduced-motion, and low-power fallbacks.
Theme-aware media tint
Image and video cards receive a subtle tint derived from theme.background by default. This keeps media aligned with the card palette without changing the source asset.
Tune the tint when the artwork needs a different treatment:
<SpinCard
background={{ src: "/images/coast.jpg" }}
theme={{ background: "#153b45", foreground: "#fff" }}
tint={{
color: "#123a46",
opacity: 0.28,
blendMode: "multiply",
}}
>
<YourComposition />
</SpinCard>Set tint={false} to preserve the original media colors. Use background.overlay or video.overlay for directional gradients; use tint for the overall color wash.
Holographic foil
Add a refractor-style spectrum, glint, and foil grain above image or video media without changing the source asset:
<SpinCard
background={{ src: "/images/collector-card.jpg" }}
holographic={{ intensity: 0.58 }}
>
<YourComposition />
</SpinCard>Pass holographic for the default treatment or tune intensity from 0 to
1. The surface combines mathematically generated guilloché waves and offset
rosettes with an aligned relief copy of the background image, so bright and dark
details shape the reflection beneath precise security-print linework. Fine
pointers move the reflected light without triggering React renders. Touch
devices receive a quiet static foil, and reduced-motion preferences disable the
shimmer movement.
Custom motion
The original two-turn whip spin is the default. Override only the values your design needs:
<SpinCard
motion={{
duration: 500,
turns: 1.5,
blur: 12,
burstDuration: 460,
}}
tilt={{ rotate: 0.5, x: 1, y: [-4, -2] }}
backFace={<BrandMonogram />}
>
<YourComposition />
</SpinCard>Set tilt={false} to disable the hover response. Motion blur is clamped between 0 and 20px.
With the fullscreen burst enabled, the animated card swells gently and dissolves directly into a borderless viewport veil; there is no expanding circle, rectangle, or stacked color wash to track. Image and video clones keep the current playback position so media does not restart during the handoff.
On Safari and other Apple WebKit browsers, Spin Card automatically approximates the centered-axis turn with two flat faces, omits the blur stage, gives fast multi-turn spins enough time to remain visible, and hands the burst to a sibling veil. The configured turn count, custom backFace, opacity, and burst are preserved. This avoids WebKit compositor artifacts that can enlarge, split, or flatten nested 3D card faces.
For a self-contained interaction, turn off the fullscreen burst. The card spins in place, removes its motion blur, and settles back into the layout before activation:
<SpinCard burst={false} onActivate={openDetails}>
<YourComposition />
</SpinCard>Client-side routing
Spin Card is router-agnostic. Inject your router's navigation functions and pass a routeKey that changes when navigation finishes:
// Next.js App Router
const router = useRouter();
const pathname = usePathname();
<SpinTransitionProvider
navigate={(href) => router.push(href)}
prefetch={(href) => router.prefetch(href)}
routeKey={pathname}
>
{children}
</SpinTransitionProvider>Without navigate, links use normal browser navigation. If routeKey does not change, the provider reveals the page after its safety timeout.
API
SpinCard
Accepts standard HTML attributes in addition to these props.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| children | ReactNode | required | Any React composition. |
| href | string | — | Renders an anchor and navigates after the transition. |
| onActivate | () => void \| Promise<void> | — | Runs a non-navigation action at the transition's activation point. Cannot be combined with href. |
| burst | boolean | true | Set to false to spin and settle without covering the viewport. |
| theme | SpinCardTheme | coral theme | Transition and card colors. |
| background | SpinCardBackground | — | Image, overlay, position, size, and repeat settings. |
| video | SpinCardVideoBackground | — | Muted video, poster, overlay, fit, position, loop, and preload settings. |
| tint | SpinCardMediaTint \| false | theme color at 0.18 | Media color, opacity, and blend mode, or false to disable. |
| holographic | SpinCardHolographic \| boolean | false | Pointer-reactive refractor foil over image or video media. |
| motion | SpinMotion | provider defaults | Per-card motion overrides. |
| tilt | SpinTilt \| false | enabled | Hover rotation and drift, or false to disable. |
| backFace | ReactNode | themed reverse | Custom reverse-side composition. |
| disabled | boolean | false | Disables activation and tilt. |
SpinTransitionProvider
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| children | ReactNode | required | The subtree containing spin triggers. |
| navigate | (href: string) => void \| Promise<void> | browser navigation | Client-router adapter. |
| prefetch | (href: string) => void \| Promise<void> | — | Called when a link card is hovered. |
| routeKey | unknown | — | Changing value starts the destination reveal. |
| motion | SpinMotion | built-in defaults | Motion defaults inherited by every card. |
| burst | boolean | true | Default burst behavior inherited by every card. |
| onCover | () => void | — | Runs when the burst covers the viewport. |
| onReveal | () => void | — | Runs after the destination is revealed. |
| navigationTimeout | number | 4000 | Safety reveal timeout in milliseconds. |
Motion defaults
{
duration: 430,
turns: 2,
blur: 16,
exitScale: 1.06,
burstDelay: 280,
burstDuration: 420,
revealDelay: 120,
revealDuration: 240,
}Motion limits
Motion values are normalized so every supported combination remains legible and returns to a clean final state:
| Value | Supported behavior |
| --- | --- |
| turns | Clamped to -4…4; negative values reverse direction. |
| duration | Clamped to 160…2000ms and raised to at least 180ms per turn. |
| blur | Clamped to 0…20px. |
| exitScale | Clamped to 0.9…1.5. |
| burstDelay | Kept within the final 58–78% of the effective spin duration. |
| burstDuration | Clamped to 160…1200ms. |
| revealDelay | Clamped to 0…2000ms. |
| revealDuration | Clamped to 0…1000ms. |
Fractional turns are supported during fullscreen bursts. In spin-only mode,
they settle on the nearest whole rotation so the card returns front-facing
without a visible snap. Apple WebKit receives at least 320ms per turn because
its transform-only fallback cannot rely on motion blur to communicate speed.
Headless trigger
Use useSpinTransition when another element should start the effect:
const { begin } = useSpinTransition();
function openFrom(element: HTMLElement) {
begin({
source: element,
onActivate: openProject,
theme: { background: "#234dde" },
backFace: <Logo />,
});
}Styling
The shell has no padding, typography, aspect ratio, or content layout. Style your composition normally and use CSS custom properties for the shell:
.my-spin-card {
--spn-radius: 2rem;
--spn-focus: #ffffff;
aspect-ratio: 4 / 5;
}During the transition, Spin Card clones the rendered DOM so CSS, images, SVG, video, and nested React output stay visually intact. Provide a video poster for the cleanest loading and transition fallback.
Accessibility
- Native anchors for links and native buttons for actions
- Normal behavior for modified clicks and
target="_blank" - Visible keyboard focus with
:focus-visible - Immediate activation when reduced motion is preferred
- Video posters replace autoplaying backgrounds when reduced motion is preferred
- Holographic foil stays static when reduced motion is preferred
- No hover animation for touch pointers
Browser support
Spin Card supports current Chrome, Edge, Firefox, and Safari releases. It uses the Web Animations API when available and falls back to immediate activation when it is not, so navigation and actions never depend on animation support.
Safari-specific compatibility includes legacy reduced-motion listeners, autoplay-safe muted video, timeout-safe animation cleanup, and a flat-face axis-spin fallback that avoids WebKit's nested-3D compositor issue.
Development
npm install
npm run dev
npm test
npm run typecheck
npm run buildThe playground includes six compositions and a shadcn/ui motion lab with local image and video uploads. Uploaded media stays in the browser and is never sent to a server.
License
MIT © Polo García
