Film Noir
Pure-CSS film grain layer: feTurbulence noise flickering in steps, horizontal scanlines and radial dark vignette — no dependency, no JS.
You'll get access to the interactive demo with a free account.
This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Backgrounds has 50 effects, including 6 free. Explore the category →
3 usage examples



How it works
The effect is a stack of four position: absolute; inset: 0 layers inside a .ng-film-scene container. The key layer is .ng-film-grain, an empty <div> whose inline style carries filter: url(#ng-film-filter). An SVG filter — <feTurbulence type="fractalNoise" baseFrequency="0.65" numOctaves="3" stitchTiles="stitch">, defined once on the page — generates a fractal noise map (Perlin noise). A <feColorMatrix> converts the noise into semi-transparent white; at opacity: 0.4, it produces the film grain texture.
The @keyframes ngFilmFlicker animation (0.1 s, steps(3)) translates .ng-film-grain without interpolation across three positions: (0,0), (−2 px, 1 px), (1 px, −1 px). steps(3) ensures hard jumps — the grain appears re-seeded each frame while the SVG map remains static, because the crop window is what moves. At 0.1 s × 3 positions, the grain shifts at ≈ 30 fps, mimicking 24–30-frame film stock.
Scanlines are a repeating-linear-gradient(0deg, transparent 2px, rgba(0,0,0,0.05) 4px) on .ng-film-lines — 4 px pitch, opacity 0.05. The vignette is a radial-gradient(transparent 50%, rgba(0,0,0,0.6) 100%) on .ng-film-vignette, darkening the edges without touching the center.
No JavaScript is involved — the effect is 100% CSS + SVG. The .ng-film-gradient layer (two coloured radial gradients) is entirely optional: swap its colours or remove it entirely to change the chromatic mood without touching the grain.
Accessibility
- No
@media (prefers-reduced-motion: reduce)block is present in the provided code: thengFilmFlickeranimation runs continuously. For motion-sensitive users, add.ng-film-grain { animation: none; }inside that media query. - The effect is purely decorative: add
role="img"and a descriptivearia-labelon.ng-film-sceneif the block carries visual meaning, otherwise mark itaria-hidden="true". - The vignette darkens edges but spares the center: white text centred on
#1a1a2eachieves a contrast ratio of ≈ 13:1 (WCAG AA confirmed). - No JavaScript, no interaction — no keyboard-navigation issues attributable to the effect itself.
- Without SVG Filter support (very old environments),
.ng-film-grainrenders transparent; scanlines and the CSS vignette remain active — graceful degradation with no errors.
Browser compatibility
Requires SVG Filters (feTurbulence) and CSS animations — supported in all modern browsers since 2016. Zero external dependencies, no JS.
Without SVG Filter support, <code>.ng-film-grain</code> renders empty (filter ignored); scanlines and the CSS vignette remain active. No JS errors.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="ng-film-scene" role="img" aria-label="Animated film grain texture — grain, scanlines, vignette">
<!-- SVG filter (once per page) -->
<svg style="display:none" aria-hidden="true">
<filter id="ng-film-filter">
<feTurbulence type="fractalNoise" baseFrequency="0.65" numOctaves="3" stitchTiles="stitch"/>
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0.3 0.3 0.3 0 0"/>
</filter>
</svg>
<div class="ng-film-gradient" aria-hidden="true"></div>
<div class="ng-film-grain" style="filter: url(#ng-film-filter)" aria-hidden="true"></div>
<div class="ng-film-lines" aria-hidden="true"></div>
<div class="ng-film-vignette" aria-hidden="true"></div>
<!-- Your overlay content (position: relative; z-index: 2) -->
<div class="ng-film-content">…</div>
</div>
Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
| .ng-film-grain — opacity: 0.4 | 0.4 | Grain intensity. 0.2 = subtle fresh stock, 0.6 = worn 16 mm film. |
| .ng-film-grain — animation-duration: 0.1s | 0.1s | Flicker speed. 0.05 s = very fast (super 8), 0.2 s = slow (dreamlike mood). |
| .ng-film-grain — steps(3) | 3 | Grain positions per cycle. 2 = very jerky, 6 = smoother transitions. |
| .ng-film-vignette — rgba(0,0,0,.6) in radial-gradient | 0.6 | Vignette intensity. 0.3 = subtle, 0.85 = full-screen cinematic framing. |
| .ng-film-vignette — transparent 50% | 50% | Extent of the unshaded central zone. 30% = tight portrait vignette, 70% = edges barely touched. |
| .ng-film-lines — rgba(0,0,0,.05) in repeating-linear-gradient | 0.05 | Scanline opacity. 0 = invisible, 0.15 = strong CRT monitor look. |
| .ng-film-lines — 4px pitch in repeating-linear-gradient | 4px | Line density. 4 px = standard-definition TV, 2 px = high-density, 6 px = coarse 1970s look. |
| .ng-film-scene — background: #1a1a2e | #1a1a2e | Container background colour. Replace with your tint: pure black, sepia, midnight blue, bordeaux. |
FAQ
<svg style="display:none"><filter id="ng-film-filter"><feTurbulence type="fractalNoise" baseFrequency="0.65" numOctaves="3" stitchTiles="stitch"/><feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0.3 0.3 0.3 0 0"/></filter></svg>. On a multi-scene page, a single definition serves all instances.position: absolute layers. Add your content in a <div> with position: relative; z-index: 2 inside .ng-film-scene. The vignette darkens the edges — place your text at the centre for optimal contrast.@keyframes animations do not pause off-screen. To save CPU on mobile, add an IntersectionObserver in JS that toggles animation-play-state: paused on .ng-film-grain when the scene leaves the viewport, and running when it returns.