Skip to content
shadcn.io

shadcn.io is not affiliated with official shadcn/ui

Backgrounds

Pro

Halftone Background for React

A grid of print dots whose sizes follow a slowly drifting field of light and shade, like a newsprint photo. For React with Base UI, when a hero or a card should feel printed, from newsprint to riso.

Framework

Halftone fills the box with a grid of dots whose sizes follow a slowly drifting field of light and shade, the way newsprint prints a photograph: big dots where the field is dark, small ones or none where it's light. The sizes come from a CSS trick rather than a canvas. Each cell is a soft radial gradient, a field of rings and bands is laid over it, and a high-contrast filter turns the sum into crisp dots; only the field moves, by CSS transforms the browser composites without repainting. A blend then inks the dots in your colour and drops the paper onto the container's own background.

Use it for print-inspired heroes, editorial pages and zine-style cards. For an even dot grid that doesn't change, use Dot Pattern; for cells that fade instead of grow, Flickering Grid; for film texture instead of print, Grain. It belongs on Paper, and Circles draws the same concentric rings as lines. For a grid pushed around by a moving wave, use Wave Grid.

Preview width

Installation

Part of Pro
Pro unlocks the code and one-line install of every Pro button and text effect, plus the MCP for your AI editor.
Have Pro already? Sign in to open it.Opens here as soon as you're Pro.

Usage

import { Halftone } from "@/components/ui/halftone";
<Halftone className="min-h-[480px]" fade="top">
  <h1>Printed in dots</h1>
</Halftone>

Halftone is a container: your content goes inside it, and the dots sit behind that content. Give it a size with className; for a whole page, className="fixed inset-0 -z-10" with no children. The paper is the container's own background (bg-background by default): the blend keeps light paper in light mode and dark paper in dark mode, so for a dark band on a light page, add the dark class to the container.

Demos

Riso Pink

dotColor takes any CSS colour, and the dots come out exactly that colour on light or dark paper. dotSize={10} gives a finer grid, intensity="strong" grows the largest dots until they touch, and fade="bottom" clears the dots at the bottom, where copy can sit.

Preview width

Inside a Card

intensity="subtle" keeps every dot under half a cell, quiet enough for a small card, and speed={0.5} halves the drift.

Preview width

Accessibility

The dots are decoration: they're hidden from assistive tech and never take the pointer, so everything you put inside works as usual. With reduced motion the field holds still, a still halftone, and in forced-colors mode the dots are hidden. Nothing flashes: the field takes 24 seconds to come round. Dots behind text make it busier, so fade them behind your copy with fade, or use intensity="subtle", and keep the text in your foreground colour.

FAQ

How does it work without a canvas?

Each cell holds a soft radial gradient, dark at its centre. A field of rings and bands is laid over the cells at half strength, and a contrast filter turns the sum into a hard-edged dot that's bigger where the field is dark. Keyframes slide and swell the field; nothing else moves.

Which props should I reach for first?

dotSize sets the grid (10 for fine print, 20 for a graphic look), intensity sets how big the largest dots get, and dotColor sets the ink. fade clears the dots toward one edge.

Can I use brand colours?

Yes. dotColor takes any CSS colour, and the dots come out exactly that colour on light or dark paper. By default they're your theme's primary colour.

How do I keep a headline readable?

Fade the dots out behind it: fade="top" for a headline at the top, fade="bottom" for copy at the bottom. Or use intensity="subtle", which keeps every dot small.

Why does the paper colour come from the container?

The dots are blended onto whatever the container paints, so the paper is its background: bg-background by default, or any colour you give it with className. Keep it light in light mode and dark in dark mode, or put the dark class on the container for a dark band on a light page.

Is it fast enough for a full-page hero?

Yes. No script runs. The dots and the field are CSS gradients drawn once; while it moves, the browser only shifts the field and reapplies the filter and blends on the compositor, the part of the browser that combines layers without repainting them.

What happens with reduced motion?

The field stops in place, so you get a still halftone with the same dots, and no loop runs.

API Reference

Halftone

<div> data-slot="halftone"

PropType
dotColor?string

The dots' ink, any CSS colour. @default "var(--primary)".

dotSize?number

The grid's spacing in pixels: 10 for fine print, 20 for a graphic look. @default 14.

Default
== undefined ? undefined : `${props.dotSize
fade?"none" | "radial" | "top" | "bottom"

Where the dots fade out: toward the top (behind a headline there), toward the bottom, toward every edge from the centre (radial), or nowhere.

Type
"none" | "radial" | "top" | "bottom"
Default
"none"
intensity?"subtle" | "medium" | "strong"

How big the largest dots get: half a cell (subtle), most of a cell (medium), or touching (strong). Ark default: medium.

Type
"subtle" | "medium" | "strong"
Default
== undefined ? undefined : DEPTH[props.intensity]
speed?number

How fast the field of light and shade drifts: 2 is twice as fast, 0.5 half as fast.

Default
1
render?ReactElement | ((props: object, state: object) => ReactElement)

Replace the host element or compose it with another component.

Type
ReactElement | ((props: object, state: object) => ReactElement)
className?string

Merged with the part's classes.

CSS variableType
--halftone-colorstring

The dots' ink (var(--primary)).

--halftone-sizestring

The grid's spacing (14px).

--halftone-speedstring

The drift's speed, a multiplier (1).

All 100 backgrounds