Browse the docs
BackgroundsNo. 90 of 109
Grain
Film grain over a surface, from a tile of SVG noise, with an optional stepped jitter.
Installation
pnpm dlx shadcn@latest add @corsair-ui/grainNo 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
clsxtailwind-merge
Examples
Over a gradient
Without grain
With grain
Animated
Old film
API
| Prop | Type | Default |
|---|---|---|
opacityStrength, 0 to 1. | number | 0.12 |
frequency / sizeFineness of the noise; tile size in px. | number | 0.8 / 180 |
blendHow it mixes with what is under it. | CSS mix-blend-mode | "overlay" |
fixed / animatedCover the viewport; jitter like film. | boolean | false |
Accessibility
- Decorative, hidden from screen readers, lets clicks through. Works as a server component.
- The jitter holds still with prefers-reduced-motion.
Compatibility
- Tailwind CSS
- 3.4 and 4, both checked in CI
- React
- 19
- Rendering
- Works in server components
Source
1 file, installed as source you own
grain.tsxView on GitHubimport type { ComponentProps, CSSProperties } from "react";
import { cn } from "@/registry/default/lib/utils";
/**
* One tile of noise as an SVG data URI. The filter id lives inside the
* image's own document, so it never clashes with ids on the page.
*/
function noiseTile(frequency: number, size: number) {
const svg =
`<svg xmlns='http://www.w3.org/2000/svg' width='${size}' height='${size}'>` +
`<filter id='n'><feTurbulence type='fractalNoise' baseFrequency='${frequency}' numOctaves='2' stitchTiles='stitch'/>` +
`<feColorMatrix values='0 0 0 0 .5 0 0 0 0 .5 0 0 0 0 .5 0 0 0 1 0'/></filter>` +
`<rect width='100%' height='100%' filter='url(#n)'/></svg>`;
return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
}
interface GrainProps extends Omit<ComponentProps<"div">, "children"> {
/** Strength of the grain, from 0 to 1. */
opacity?: number;
/** Fineness of the noise (the filter's `baseFrequency`); higher is finer. */
frequency?: number;
/** How the grain mixes with what is under it: any CSS `mix-blend-mode`. */
blend?: CSSProperties["mixBlendMode"];
/** Cover the whole viewport instead of the nearest positioned parent. */
fixed?: boolean;
/** Width and height of one noise tile, in px. */
size?: number;
/** Jitter the grain in small steps, like film running through a projector. */
animated?: boolean;
}
/**
* Film grain laid over a surface: a tile of SVG noise repeated as a
* background image, so there is nothing to download and no script to run.
* It works as a server component and any number can share a page. It is
* decorative, hidden from screen readers and lets every click through.
* With `animated`, the grain jitters in steps; `prefers-reduced-motion`
* keeps it still. Put it inside a positioned element, after the content it
* covers, or pass `fixed` for the whole page.
*
* @example
* <section className="relative overflow-hidden rounded-xl bg-muted p-10">
* <h2>Old harbour</h2>
* <Grain opacity={0.2} animated />
* </section>
*/
function Grain({
opacity = 0.12,
frequency = 0.8,
blend = "overlay",
fixed = false,
size = 180,
animated = false,
className,
style,
ref,
...props
}: GrainProps) {
const tile = Math.max(1, Math.round(size));
return (
<div
ref={ref}
aria-hidden="true"
data-slot="grain"
data-animated={animated || undefined}
className={cn(
"pointer-events-none overflow-hidden",
fixed ? "fixed inset-0 z-50" : "absolute inset-0",
className
)}
style={{ opacity: Math.min(Math.max(opacity, 0), 1), mixBlendMode: blend, ...style }}
{...props}
>
<div
data-slot="grain-noise"
// Twice the size of the surface, so the jitter never shows an edge.
className={cn("absolute", animated ? "motion-safe:animate-grain inset-[-50%]" : "inset-0")}
style={{ backgroundImage: noiseTile(frequency, tile), backgroundSize: `${tile}px` }}
/>
</div>
);
}
export { Grain, type GrainProps };