shadcn.io is not affiliated with official shadcn/ui
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.
Installation
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.
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.
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.
API Reference
Halftone
<div> data-slot="halftone"
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.
--halftone-colorstring
The dots' ink (var(--primary)).
--halftone-sizestring
The grid's spacing (14px).
--halftone-speedstring
The drift's speed, a multiplier (1).
Related


Dot Pattern
GridsA static even dot grid
- Pro


Cells that fade instead of grow
- Pro


Grain Background
TextureFilm texture in place of print
- Pro


Paper Background
TextureThe sheet a print effect belongs on
- Pro


Wave Grid Background
WavesA grid displaced by a moving wave
- Pro


Circles Background
LightThe concentric rings of our field, as lines