GodUIGodUI
129Follow on X

Anatomy of Lamp

How two mirrored conic cones, a glowing seam, and a delayed children rise compose a scroll-triggered lamp that ignites once and stays lit.

01/04Two cones, one seam
Left cone
conic from 290deg, origin-left
Right cone
conic from 70deg, origin-right
Bar + wash
scaleX seam + radial ellipse
Children
slot rises into the light
tsx
style={{ ["--lamp" as string]: color, ...style }}// Right cone — mirrored from the leftclassName="… origin-right [background-image:conic-gradient(from_70deg_at_center_top,color-mix(in_oklch,var(--lamp)_60%,transparent),transparent,transparent)] [mask-image:linear-gradient(to_top,transparent,white)]"// Left coneclassName="… origin-left [background-image:conic-gradient(from_290deg_at_center_top,transparent,transparent,color-mix(in_oklch,var(--lamp)_60%,transparent))] …"
01

Two cones, one seam

Lamp is a viewport-triggered light: two mirrored conic gradients widen from a center seam, a glowing bar and radial wash bloom where they meet, and the children rise into that light a beat later. All of the motion runs on the compositor (scaleX, opacity, y), driven by Framer Motion whileInView.

The silhouette is four layers stacked in a single isolate stage. A right cone (conic-gradient(from 70deg…), origin-right) mirrors a left cone (from 290deg, origin-left). Where they meet, a thin blurred bar scales on the X axis, and a radial ellipse wash softens the join. Children sit in a raised slot below the light.

Accent color flows through one CSS variable: --lamp, set from the color prop (default var(--primary)). Cones mix it at 60% into transparent; the wash mixes at 25%.

Masks fade each cone toward the bottom so the light reads as a beam falling onto the stage, not a hard triangle.

02/04The ignite: scaleX from half to full

initial={unlit} whileInView={lit} · viewport once −20%

Cones
scaleX 0.5→1 · opacity 0.5→1
Bar
scaleX 0.5→1, same 0.8s easeInOut
Viewport
once:true, margin −20%
tsx
const lit = { scaleX: 1, opacity: 1 };const unlit = { scaleX: 0.5, opacity: 0.5 };const VIEWPORT = { once: true, margin: "-20%" } as const;<motion.divinitial={reduceMotion ? lit : unlit}whileInView={lit}viewport={VIEWPORT}transition={{ duration: 0.8, ease: "easeInOut" }}/>
02

The ignite: scaleX from half to full

Both cones start unlit (scaleX: 0.5, opacity: 0.5) and tween to scaleX: 1, opacity: 1 over 0.8s easeInOut. The bar uses the same duration but only animates scaleX. Because origins sit on opposite sides of the seam, the light blooms outward from the center rather than growing from a shared pivot.

viewport.once: true with margin: "-20%" means the section must be well into the viewport before ignition, and it never re-runs on scroll-away. This is deliberate: a lamp that re-ignites every time you scroll past looks broken.

Animating width on a 30rem cone would thrash layout every frame. scaleX stays on the compositor, keeps the conic gradient resolution stable, and lets origin-left / origin-right do the directional work for free. The radial wash is a static div with no animation of its own; it reads brighter as the cones cover more of it.

03/04Children rise after the light

transition={{ duration: 0.8, delay: 0.2, ease: "easeInOut" }}

Light
cones + bar ignite first (t=0)
Children
opacity + y:40→0, delay 0.2s
Reduced motion
start at lit + risen state
tsx
<motion.divinitial={reduceMotion ? { opacity: 1, y: 0 } : { opacity: 0, y: 40 }}whileInView={{ opacity: 1, y: 0 }}viewport={VIEWPORT}transition={{ duration: 0.8, delay: 0.2, ease: "easeInOut" }}>{children}</motion.div>
03

Children rise after the light

The headline slot starts at opacity: 0, y: 40 and settles to opacity: 1, y: 0 on the same 0.8s ease, with a 0.2s delay. The light arrives first; content follows into an already-lit stage.

04/04Result

Build something
the world remembers

04

The result

Two mirrored cones, one seam, and one delayed rise, ignited once by whileInView and colored by --lamp. Every animated property runs on the compositor.

Reduced motion

useReducedMotion() swaps every initial to the lit / risen end state. The lamp still paints its cones, bar, wash, and children, but nothing animates. There is no separate static poster: reduced-motion users see the finished composition.

Motion Score

LampSS: Compositor-only
StranslatePosition / lift via translate
SscaleScale spring / press
SopacityFade / cross-fade
Each property is graded by how the browser runs it, from S (composited off the main thread) down to F (layout thrashing); the component takes the worst. MotionScore methodology →