Browse the docs
MotionNo. 108 of 145
Tilt Scroll
Content that starts tilted back in perspective and straightens as you scroll, with CSS scroll-driven animations and no JavaScript.
Installation
pnpm dlx shadcn@latest add @corsair-ui/tilt-scrollNo setup needed: the CLI finds @corsair-ui in the shadcn registry directory. The files land in your project as source: edit them like your own code. More in Installation.
- Also adds
- utils
- npm
@radix-ui/react-slotclsxtailwind-merge
Examples
A dashboard that straightens
Scroll it into view; flat below 768px with flatBelow="md".
Berths booked
82%
Open hours
22 h
Waiting list
7
Steeper, with a shorter range
angle={35}, scale={0.8}, perspective={800} and range="entry 10% cover 50%".
Tilted
Next stop
Havana, 20:00
API
| Prop | Type | Default |
|---|---|---|
angle / scale / perspectiveThe starting lean in degrees, size and depth in px. | number | 20 / 0.9 / 1200 |
timelineThe element crossing the viewport, or the page scroll from the top (for a hero in view at load). | "view" | "root" | "view" |
rangeThe animation-range the straightening spans. | string | "entry 0% cover 40%" / "0px 60vh" |
flatBelowStay flat below this breakpoint, by CSS alone. | "sm" | "md" | "lg" | false | false |
asChildTilt the child itself. | boolean | false |
Styling
Restyle it in your project: className on each part, the theme variables, and these attributes with arbitrary variants, e.g. className="[&_[data-slot=tilt-scroll]]:text-primary".
- data-slot
tilt-scroll
- State
data-timelinecomputed
Accessibility
- Pure CSS; browsers without scroll-driven animations, and prefers-reduced-motion, show it flat.
Compatibility
- Tailwind CSS
- 3.4 and 4, both checked in CI
- React
- 19
- Rendering
- Works in server components
- Motion
- Respects prefers-reduced-motion
Source
1 file, installed as source you own
tilt-scroll.tsxView on GitHubimport { Slot } from "@radix-ui/react-slot";
import type { ComponentProps, CSSProperties } from "react";
import { cn } from "@/registry/default/lib/utils";
// Literal classes, so Tailwind finds them in the source.
const FLAT_BELOW = {
sm: "max-sm:!animate-none",
md: "max-md:!animate-none",
lg: "max-lg:!animate-none",
} as const;
const DEFAULT_RANGE = {
// From the first pixel entering until it has covered 40% of its crossing.
view: "entry 0% cover 40%",
// A hero is already on screen at load: straighten over the first 60vh of page scroll.
root: "0px 60vh",
} as const;
interface TiltScrollProps extends ComponentProps<"div"> {
/** Apply the tilt to the single child element instead of wrapping it in a div. */
asChild?: boolean;
/** How far it leans back at the start, in degrees around the horizontal axis. */
angle?: number;
/** How large it starts, as a fraction of its size. */
scale?: number;
/** Distance to the viewer for the 3D effect, in px. Smaller is more dramatic. */
perspective?: number;
/**
* What drives it: "view" is the element crossing the viewport, "root" the
* page's scroll from the top, for a hero that is in view at load.
*/
timeline?: "view" | "root";
/**
* The CSS `animation-range` the straightening spans. Defaults to
* "entry 0% cover 40%" for "view" and "0px 60vh" for "root".
*/
range?: string;
/** Below this breakpoint it stays flat, by CSS alone. */
flatBelow?: keyof typeof FLAT_BELOW | false;
}
/**
* Starts content tilted back in perspective, slightly smaller, and
* straightens it as you scroll: the "container scroll" hero where a product
* screenshot rises to face the reader. It runs on CSS scroll-driven
* animations, so there is no JavaScript and no scroll listener, and it works
* in server components. Browsers without scroll timelines, `prefers-reduced-motion`
* and viewports below `flatBelow` show it flat, its finished state.
*
* It tilts from the top edge (`origin-top`); pass another `origin-*` class to
* change that. It follows the nearest scroll container with
* `timeline="view"`, and `overflow: hidden` makes one that never scrolls:
* clip a wrapper with `overflow-clip` instead.
*
* @example
* <TiltScroll timeline="root" flatBelow="md" className="mx-auto max-w-5xl">
* <BrowserFrame url="app.example.com">
* <img src="/dashboard.png" alt="The dashboard" />
* </BrowserFrame>
* </TiltScroll>
*/
function TiltScroll({
asChild = false,
angle = 20,
scale = 0.9,
perspective = 1200,
timeline = "view",
range,
flatBelow = false,
className,
style,
...props
}: TiltScrollProps) {
const Comp = asChild ? Slot : "div";
return (
<Comp
data-slot="tilt-scroll"
data-timeline={timeline}
className={cn(
"supports-[animation-timeline:view()]:animate-tilt-scroll origin-top motion-reduce:!animate-none",
flatBelow ? FLAT_BELOW[flatBelow] : undefined,
className
)}
style={
{
"--tilt-scroll-angle": `${angle}deg`,
"--tilt-scroll-scale": scale,
"--tilt-scroll-perspective": `${perspective}px`,
// After the utility's shorthand, which would reset them.
animationTimeline: timeline === "root" ? "scroll(root)" : "view()",
animationRange: range ?? DEFAULT_RANGE[timeline],
...style,
} as CSSProperties
}
{...props}
/>
);
}
export { TiltScroll, type TiltScrollProps };