Your headline is built from thousands of grains of sand. The grains drift on the breeze, erode where the cursor sweeps through them, and spring back to spell the word again.
import SandTypography from '@crazygl/hero-sand-typography';
export default function App() {
return (
<SandTypography
heading="Sands of time."
subheading="Sweep your cursor and watch the wind do its work."
ctaLabel="Begin the journey →"
grainCount={8000}
/>
);
}heading, optional subheading, and a CTA (ctaLabel, onCTAClick as URL or function, ctaTextColor, ctaBgColor).grainCount (2000–15000, sweet spot 6000–10000), grainSize, and the grainColorLight / grainColorDark palette.springStiffness, springDamping, brownianJitter tune how grains recover and how alive the field feels at rest.windRadius, windStrength, windDecayTime control the cursor gust.headingFontFamily, headingFontWeight, headingSize, headingOffsetX / headingOffsetY.ambientGrainCount, ambientGrainOpacity, ambientGrainSpeed for the background drift layer.bgTop / bgBottom dune gradient or transparentBackground.npm install @crazygl/hero-sand-typographyThe component takes the same props you see in the live customizer on the right — every default ships poster-quality.
import SandTypography from '@crazygl/hero-sand-typography';
export default function Landing() {
return (
<SandTypography />
);
}The wrapper renders static HTML on the server and only initialises the canvas after hydration, so search engines see your copy.
// app/page.tsx — works in SSR-first frameworks (Next, Remix, Astro, etc.)
'use client';
import SandTypography from '@crazygl/hero-sand-typography';
export default function Page() {
return (
<section>
<SandTypography
heading="Say hi."
subheading="Your new hero."
/>
<article>
<h2>Welcome</h2>
<p>Your content keeps its own voice below the hero.</p>
</article>
</section>
);
}