A film-grain layer made from an SVG turbulence filter, with strength, fineness, blend mode and an optional shimmer, to take the plastic edge off gradients, cards and photos.
Edition 04
Grain makes it feel printed
import { NoiseTexture } from "@/components/ballmac/noise-texture"
export default function NoiseTextureDemo() {
return (
<div className="relative flex h-64 w-full max-w-xl items-end overflow-hidden rounded-2xl border bg-[linear-gradient(135deg,var(--chart-4),var(--chart-1)_55%,var(--chart-2))] p-6">
<NoiseTexture opacity={0.22} />
<div className="relative">
<p className="font-mono text-xs tracking-widest text-white/80 uppercase">Edition 04</p>
<h3 className="mt-1 text-3xl font-semibold tracking-tight text-white drop-shadow-sm">Grain makes it feel printed</h3>
</div>
</div>
)
}Installation
$ pnpm dlx shadcn@latest add @ballmac/noise-textureInstall the dependencies.
$ pnpm add motion@^12Copy the source into your project.
components/ballmac/noise-texture.tsx// Ballmac UI: Noise Texture. https://ui.ballmac.com/components/noise-texture "use client" import * as React from "react" import { motion, useReducedMotion } from "motion/react" import { cn } from "@/lib/utils" type NoiseTextureProps = Omit<React.ComponentProps<"div">, "children"> & { /** Strength of the grain, from 0 to 1. */ opacity?: number /** Fineness of the grain. Higher is finer. */ frequency?: number /** Side of the repeating tile in pixels. */ tile?: number /** How the grain mixes with what is behind it. "overlay" adds grain to light and dark areas alike. */ blend?: "overlay" | "soft-light" | "multiply" | "screen" | "normal" /** Make the grain shimmer like film. */ animated?: boolean } function noiseImage(frequency: number, tile: number) { const svg = `<svg xmlns='http://www.w3.org/2000/svg' width='${tile}' height='${tile}'><filter id='n' x='0' y='0' width='100%' height='100%'><feTurbulence type='fractalNoise' baseFrequency='${frequency}' numOctaves='3' stitchTiles='stitch'/><feColorMatrix type='saturate' values='0'/></filter><rect width='100%' height='100%' filter='url(#n)'/></svg>` return `url("data:image/svg+xml,${encodeURIComponent(svg)}")` } /** A film-grain layer for gradients, cards and photos. Put it inside a positioned parent. Decorative. */ function NoiseTexture({ opacity = 0.14, frequency = 0.8, tile = 220, blend = "overlay", animated = false, className, style, ...props }: NoiseTextureProps) { const reduce = useReducedMotion() const image = React.useMemo(() => noiseImage(frequency, tile), [frequency, tile]) const steps = [0, 0.3, 0.6, 0.15, 0.85, 0.45].map((n) => `${Math.round(n * tile)}px ${Math.round(((n * 7) % 1) * tile)}px`) return ( <div data-slot="noise-texture" aria-hidden="true" className={cn("pointer-events-none absolute inset-0 overflow-hidden", className)} style={{ ...style }} {...props} > <motion.div className="absolute -inset-[20%]" style={{ backgroundImage: image, backgroundSize: `${tile}px ${tile}px`, opacity, mixBlendMode: blend }} animate={animated && !reduce ? { backgroundPosition: steps } : undefined} transition={{ duration: 0.9, repeat: Infinity, ease: "linear", times: [0, 0.2, 0.4, 0.6, 0.8, 1], type: "keyframes" }} /> </div> ) } export { NoiseTexture, type NoiseTextureProps }Update the import paths to match your project setup.
Usage
import { NoiseTexture } from "@/components/ballmac/noise-texture"The full example is in the Code tab above.
Examples
Blend modes
import { NoiseTexture } from "@/components/ballmac/noise-texture"
const blends = ["overlay", "soft-light", "multiply", "normal"] as const
export default function NoiseTextureBlends() {
return (
<div className="grid w-full max-w-xl grid-cols-2 gap-3 sm:grid-cols-4">
{blends.map((blend) => (
<div key={blend} className="relative h-28 overflow-hidden rounded-xl border bg-[linear-gradient(160deg,var(--chart-3),var(--chart-5))]">
<NoiseTexture blend={blend} opacity={0.3} />
<span className="relative block p-2 font-mono text-[11px] text-white drop-shadow-sm">{blend}</span>
</div>
))}
</div>
)
}API reference
| Prop | Type | Default |
|---|---|---|
opacityStrength of the grain, from 0 to 1. | number | 0.14 |
frequencyFineness of the grain. Higher is finer. | number | 0.8 |
tileSide of the repeating tile in pixels. | number | 220 |
blendHow the grain mixes with what is behind it. "overlay" adds grain to light and dark areas alike. | "overlay" | "soft-light" | "multiply" | "screen" | "normal" | "overlay" |
animatedMake the grain shimmer like film. | boolean | false |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Screen readers | Decorative: aria-hidden and pointer-events-none |
| Reduced motion | The grain never shimmers |
Use with AI
<NoiseTexture opacity frequency tile blend animated /> absolutely fills its relative parent. No image file is needed; the grain is an inline SVG data URI. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Noise Texture (@ballmac/noise-texture) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Gradient heroes and cards that band or look flat
- Giving imagery a printed feel
Not for
- Text-heavy surfaces at high opacity
Registry JSON: https://ui.ballmac.com/r/noise-texture.json
Credits
Free to use in personal and commercial projects.
- npm
- motion@^12
- Registry
- shadcn/utils
Pairs well with
Aurora Background
Soft, slowly drifting aurora light made of blurred token-colored glows with fine vertical curtains and an optional radial fade. Pure CSS layers, paused off-screen, still under reduced motion.
Gradient Text
Text filled with a gradient of theme colors that slowly drifts, or a shiny variant that sweeps a glint of light across it on a pause. Static under reduced motion; plain text in forced colors.
Card
A composable content surface with compact spacing, an action slot, and optional interactive feedback.
Beams Background
Hairline rails with thin light beams rising along them and dissolving near the top, over a soft glow. Canvas, token colors, DPR aware, pauses off-screen, a still frame under reduced motion.