Skip to content
UI Kit
Accent color

Noise

A grain overlay from an SVG turbulence that takes the plastic edge off gradients and accent surfaces.

One of 6 components in Backgrounds

3 variants

Variants

Soft grain

Fine grain over a gradient mesh, like paper under light.

Film grain

Coarser grain that flickers like film. Stands still with reduced motion.

On an accent card

Grain in the contrast color on a solid accent surface, with a soft blend.

Details

What Noise brings along, read from the registry and the source.

Category
Backgrounds
Variants
3
Version
1.0.0 · since 1.0.0
File
components/ui/noise.tsx
Brings along
utilskit
npm packages
None
Languages
Texts come through props
Studio
Adjust in code

Props

The NoiseProps interface from components/ui/noise.tsx, with the comments from the code.

  • opacityoptional
    number

    0 to 1, default 0.4. The grain itself is already thin, 1 is still subtle.

  • blendoptional
    NoiseBlend

    normaloverlaysoft-lightmultiplyscreen

    How the grain mixes with what is below, default normal

  • grainoptional
    NoiseGrain

    finemediumcoarse

    Size of the grain, default fine

  • toneoptional
    'foreground' | 'background' | 'contrast'

    Color of the grain: foreground (dark specks in light mode, light in dark), background (the opposite) or contrast (primary-foreground, for grain on an accent surface)

  • animatedoptional
    boolean

    Let the grain flicker like film, off with reduced motion

Accessibility

Read from the source of the component.

  • Needs no client state. The server writes it in full, so it reads without JavaScript too.

Motion

  • Brings animations of its own, calmly timed and without wobble.
  • With reduced motion (prefers-reduced-motion) it stays still.

Source

The file aiw add puts into your project. From then on it is yours.

Show the file, 79 lines
components/ui/noise.tsx
'use client';

import { cn } from '@/lib/kit';

export type NoiseBlend = 'normal' | 'overlay' | 'soft-light' | 'multiply' | 'screen';
export type NoiseGrain = 'fine' | 'medium' | 'coarse';

export interface NoiseProps {
  /** 0 to 1, default 0.4. The grain itself is already thin, 1 is still subtle. */
  opacity?: number;
  /** How the grain mixes with what is below, default normal */
  blend?: NoiseBlend;
  /** Size of the grain, default fine */
  grain?: NoiseGrain;
  /**
   * Color of the grain: foreground (dark specks in light mode, light in dark),
   * background (the opposite) or contrast (primary-foreground, for grain on an accent surface)
   */
  tone?: 'foreground' | 'background' | 'contrast';
  /** Let the grain flicker like film, off with reduced motion */
  animated?: boolean;
  className?: string;
}

const FREQ: Record<NoiseGrain, number> = { fine: 0.9, medium: 0.68, coarse: 0.45 };
const TONE = { foreground: 'var(--foreground)', background: 'var(--background)', contrast: 'var(--primary-foreground)' };

/*
 * The grain is an SVG turbulence turned into an alpha map and used as a mask,
 * so its color comes from a token and follows light and dark mode.
 */
const grainUrl = (freq: number, alpha: string) =>
  `url("data:image/svg+xml;utf8,${encodeURIComponent(
    `<svg xmlns='http://www.w3.org/2000/svg' width='220' height='220'><filter id='n' x='0' y='0'><feTurbulence type='fractalNoise' baseFrequency='${freq}' numOctaves='3' stitchTiles='stitch'/><feColorMatrix type='matrix' values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  ${alpha}'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>`,
  ).replace(/%2523/g, '%23')}")`;

const CSS = `
@keyframes kitfx-noise-shift {
  0% { transform: translate3d(0, 0, 0); }
  25% { transform: translate3d(-37px, 23px, 0); }
  50% { transform: translate3d(19px, -41px, 0); }
  75% { transform: translate3d(-13px, -17px, 0); }
  100% { transform: translate3d(31px, 11px, 0); }
}
@media (prefers-reduced-motion: reduce) { .kitfx-noise > * { animation: none !important; } }
`;

/**
 * A grain overlay that takes the edge off flat gradients and photos. Put it
 * after the background it should cover, it fills the parent and never takes
 * clicks. Pure CSS and SVG, no canvas.
 */
export function Noise({ opacity = 0.4, blend = 'normal', grain = 'fine', tone = 'foreground', animated, className }: NoiseProps) {
  // Dark specks where the noise is high, light specks where it is low: the grain adds texture, never a grey veil
  const dark = grainUrl(FREQ[grain], '0 0 0 2 -1.06');
  const light = grainUrl(FREQ[grain], '0 0 0 -2 0.94');
  return (
    <div aria-hidden className={cn('kitfx-noise pointer-events-none absolute inset-0 overflow-hidden', className)} style={{ opacity, mixBlendMode: blend }}>
      {animated && <style dangerouslySetInnerHTML={{ __html: CSS }} />}
      {[
        { mask: dark, color: TONE[tone] },
        { mask: light, color: tone === 'foreground' ? TONE.background : TONE.foreground },
      ].map((layer, i) => (
        <div
          key={i}
          className={cn('absolute', animated ? '-inset-12' : 'inset-0')}
          style={{
            background: layer.color,
            maskImage: layer.mask,
            WebkitMaskImage: layer.mask,
            maskSize: '220px 220px',
            WebkitMaskSize: '220px 220px',
            animation: animated ? 'kitfx-noise-shift 1.6s steps(1, end) infinite' : undefined,
          }}
        />
      ))}
    </div>
  );
}

Frequent questions

How do I install Noise?

With aiw add @ai-website/noise. The command puts components/ui/noise.tsx into your project, the code is yours from then on. It needs no further npm packages.

Which variants does Noise have?

3 variants, Soft grain, Film grain and On an accent card.

Can an AI assistant build with Noise?

Yes. Through the MCP server of the aiw CLI, Claude, Cursor, GitHub Copilot, Windsurf, Codex and Gemini find Noise with aiw_components and install it with aiw_add @ai-website/noise. Name the variant in your prompt, for example variant="vivid", and the assistant writes your content straight into the props.

Often searched

Jump to

Copied to the clipboard