68 / Scenes
Grain Overlay
Film grain for any positioned box, which incidentally fixes the banding a wide gradient shows on a good monitor.
Reel one, take four
A flat gradient bands on wide screens. The grain hides the steps and gives the panel a surface.
"use client"
import { GrainOverlay } from "mischief-ui/grain-overlay"
export function GrainOverlayDemo() {
return (
<div className="border-border relative isolate w-full max-w-md overflow-hidden rounded-[var(--radius)] border">
<div className="from-primary/70 to-background bg-gradient-to-br px-6 py-14">
<h3 className="text-2xl font-semibold">Reel one, take four</h3>
<p className="text-muted-foreground mt-2 text-sm">
A flat gradient bands on wide screens. The grain hides the steps and
gives the panel a surface.
</p>
</div>
<GrainOverlay />
</div>
)
}Installation
Copy the source into your project, or keep it behind a package.
npx shadcn@latest add Tinkerers-Labs/mischief-ui/grain-overlayimport { GrainOverlay } from "mischief-ui/grain-overlay"Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.
"use client" import * as React from "react"import { cn } from "@/lib/utils" export type GrainOverlayProps = React.HTMLAttributes<HTMLDivElement> & { /** Higher is finer. Around 0.65 reads as film, 0.2 as coarse paper. */ frequency?: number opacity?: number blend?: "overlay" | "soft-light" | "multiply" | "screen" | "normal" /** Shifts the grain a few times a second the way projected film does. */ animated?: boolean} Usage
export function Panel() {
return (
<div className="relative isolate overflow-hidden">
<div className="bg-gradient-to-br from-primary to-background p-12">
<h2>Reel one, take four</h2>
</div>
<GrainOverlay />
</div>
)
}The practical reason to use it
A gradient across a wide screen has fewer available steps than it has pixels, so it arrives in visible bands. Adding noise breaks the boundary between one step and the next, and the eye stops finding the edges. This is the same trick print has used for a century, and it is the reason to reach for grain even when you do not want the texture.
It sits above the content and ignores the pointer, so it can be dropped into a card or a hero without changing anything underneath it. The parent needs a positioning context and, usually, overflow hidden.
Still by default
Animated grain is a full repaint several times a second for an effect most readers will not consciously notice, so it is off unless you ask. When it is on it stops entirely under reduced motion, because a texture that crawls is exactly the kind of movement that setting exists to remove.
API
frequencynumberHigher is finer. Around 0.65 reads as film and 0.2 as coarse paper. Defaults to 0.65.opacitynumberStrength of the grain. Defaults to 0.22.blend"overlay" | "soft-light" | "multiply" | "screen" | "normal"How the grain mixes with what is underneath. Defaults to "overlay".animatedbooleanShifts between four grains about eight times a second, the way projected film does. Off by default.Accessibility
Hidden from assistive technology and transparent to the pointer. The animated variant does nothing at all when reduced motion is set.