Load Rise
A block that rises into place as the page paints, in plain CSS, before any script has loaded.
$npx shadcn@latest add https://framebrick.co/r/load-rise.jsonFrom the Semono template, see it in context.
Photography studio
Work that earns its place on the page
It starts with the first paint, before any script has loaded.
import { LoadRise } from "@/components/ui/load-rise";
const button =
"inline-flex h-9 items-center justify-center rounded-md px-4 text-sm font-medium outline-none transition-colors focus-visible:ring-[3px] focus-visible:ring-ring/50";
/* A hero as the page opens: the label, the title, the lead and the buttons rise 0.1, 0.2, 0.4 and 0.5s apart.
Press Replay: it runs again. */
export default function LoadRiseDemo() {
return (
<div className="flex max-w-sm flex-col items-center gap-3 text-center">
<LoadRise as="p" delay={0.1} className="text-sm font-medium text-muted-foreground">
Photography studio
</LoadRise>
<LoadRise as="h2" delay={0.2} className="text-3xl font-semibold tracking-tight text-balance">
Work that earns its place on the page
</LoadRise>
<LoadRise as="p" delay={0.4} className="text-sm text-muted-foreground">
It starts with the first paint, before any script has loaded.
</LoadRise>
<LoadRise delay={0.5} className="flex gap-3 pt-2">
<a href="#" className={`${button} bg-primary text-primary-foreground hover:bg-primary/90`}>
Book a call
</a>
<a href="#" className={`${button} border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground`}>
View work
</a>
</LoadRise>
</div>
);
}Installation
$ npx shadcn@latest add https://framebrick.co/r/load-rise.json1. Install the dependencies.
npm install motion clsx tailwind-merge2. Add
cn()tolib/utils.ts(skip if shadcn already created it).import { clsx, type ClassValue } from "clsx"; import { extendTailwindMerge } from "tailwind-merge"; /* tailwind-merge must know the site's type scale (globals.css) is a font size, not a text color — otherwise cn("text-h6 text-muted-foreground") drops text-h6. */ const twMerge = extendTailwindMerge({ extend: { classGroups: { "font-size": [{ text: ["display", "h1", "h2", "h3", "h4", "h5", "h6"] }] } }, }); export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }3. Copy the component into
components/ui/load-rise.tsx.import * as React from "react"; import { cn } from "@/lib/utils"; /* * Load Rise — a block that rises into place as the page paints, once (from the Semono page-load intro, which is Framer's * own: the header and the hero's label, title, lead and buttons). It comes up 60px out of nothing along spring E * (stiffness 250, damping 55, mass 2.9: about a second, 960ms on Framer's measurement), `delay` seconds late — a hero * staggers its parts 0.1, 0.2, 0.4 and 0.5s. * * It is plain CSS, so it starts with the first paint, before any script has loaded, and a page without scripts shows * its content. That is what the other appear effects cannot do: text-appear and scroll-reveal start when the page * hydrates, and their content waits at opacity 0 until then. Use this for what is on screen as the page opens; use those * for what comes into view later. Do not wrap one in the other. * * The spring is a CSS linear() curve — 29 stops measured on Framer, within 1% of what tools/ui/spring-curve.mjs makes from * the same physics; a browser without linear() (before Chrome 113, Firefox 112, Safari 17.2) gets cubic-bezier(0.22, 1, * 0.36, 1). Reduced motion: the same fade without the rise. Print: no animation, so a printed page is never half drawn. * * The rules live in one <style>, which React 19 moves to the head once however many blocks use it (href + precedence). */ const CSS = `@keyframes fb-load-rise{from{opacity:.001;transform:translateY(var(--load-rise-distance,60px))}} @keyframes fb-load-fade{from{opacity:.001}} [data-load-rise]{animation:fb-load-rise 960ms cubic-bezier(.22,1,.36,1) backwards;animation-timing-function:linear(0,.041,.134,.247,.361,.469,.564,.646,.715,.772,.819,.857,.887,.911,.93,.946,.958,.967,.974,.98,.984,.988,.991,.993,.994,.996,.997,.997,1)} @media (prefers-reduced-motion:reduce){[data-load-rise]{animation-name:fb-load-fade}} @media print{[data-load-rise]{animation:none}}`; export type LoadRiseProps = { /** The element to render. */ as?: "div" | "section" | "header" | "h1" | "h2" | "h3" | "p" | "span" | "ul" | "li" | "figure" | "nav"; /** Seconds before it starts. */ delay?: number; /** How far it rises, in px. */ distance?: number; className?: string; children?: React.ReactNode; }; export function LoadRise({ as: Tag = "div", delay = 0, distance = 60, className, children }: LoadRiseProps) { return ( <> <style href="framebrick-load-rise" precedence="default"> {CSS} </style> <Tag data-load-rise="" className={cn(className)} style={{ animationDelay: `${delay}s`, ["--load-rise-distance" as string]: `${distance}px` }} > {children} </Tag> </> ); }
Usage
import { LoadRise } from "@/components/ui/load-rise";
<LoadRise as="p" delay={0.1}>Photography studio</LoadRise>
<LoadRise as="h1" delay={0.2} className="text-5xl font-semibold">Work that earns its place on the page</LoadRise>
<LoadRise as="p" delay={0.4}>A studio for brands that would rather be useful than loud.</LoadRise>
<LoadRise delay={0.5} className="flex gap-3">…buttons…</LoadRise>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| as | "div" | "section" | "header" | "h1" | "h2" | "h3" | "p" | "span" | "ul" | "li" | "figure" | "nav" | "div" | The element to render. |
| delay | number | 0 | Seconds before it starts. A hero staggers its parts 0.1, 0.2, 0.4 and 0.5 s, as Semono's does. |
| distance | number | 60 | How far it rises, in px. |
| className | string | — | Classes for the element. |
- It is plain CSS, so it starts with the first paint, before any script has loaded, and a page without scripts shows its content. text-appear and scroll-reveal start when the page hydrates and their content waits at opacity 0 until then: use this for what is on screen as the page opens, and those for what comes into view later. Do not wrap one in the other.
- Spring E (stiffness 250, damping 55, mass 2.9: about a second) as a 29-stop linear() curve measured on Framer, over 960 ms — within 1% of what tools/ui/spring-curve.mjs makes from the same physics. A browser without linear() gets cubic-bezier(0.22, 1, 0.36, 1) over the same time.
- It starts from opacity 0.001 and 60 px lower (Framer's 0.001 keeps the block in the render tree). animation-fill-mode backwards holds that first frame during the delay; afterwards the block is exactly as styled.
- Reduced motion: the same fade without the rise. Print: no animation, so a printed page is never half drawn.
- The rules are one <style> that React 19 moves to the head once however many blocks use it (href and precedence). With React 18 each block repeats the same few lines.
AI prompt
Paste into v0, Lovable, Bolt or Claude to rebuild this component in your own project.
Create a React + Tailwind CSS component called LoadRise — plain CSS, no JavaScript and no animation library, so it also works in a server component.
- Props: as (div by default; or section, header, h1, h2, h3, p, span, ul, li, figure, nav), delay (seconds), distance (px, default 60), className, children.
- Render <style href="framebrick-load-rise" precedence="default"> holding the rules (React 19 moves it to the head once and dedupes it) and the element with a data-load-rise attribute, style animationDelay set to the delay in seconds and a --load-rise-distance custom property.
- The rules: @keyframes fb-load-rise { from { opacity: .001; transform: translateY(var(--load-rise-distance, 60px)) } } and @keyframes fb-load-fade { from { opacity: .001 } }. [data-load-rise] { animation: fb-load-rise 960ms cubic-bezier(.22,1,.36,1) backwards; animation-timing-function: linear(0,.041,.134,.247,.361,.469,.564,.646,.715,.772,.819,.857,.887,.911,.93,.946,.958,.967,.974,.98,.984,.988,.991,.993,.994,.996,.997,.997,1) } — the list is a spring (stiffness 250, damping 55, mass 2.9) sampled at 29 stops, on its own line so a browser without linear() ignores it and keeps the cubic-bezier. Under prefers-reduced-motion: reduce use animation-name fb-load-fade; under print use animation: none.










