Backgrounds

Halftone Background

Halftone background React component: a continuous halftone dot background whose density shifts with scroll progress and whose dots swell near the cursor, drawn with CSS gradients.

A full-bleed printed halftone texture. The dot lattice is a repeating CSS radial-gradient, so no images are needed; a second mask-image layer creates a one-sided dense-to-sparse tone ramp, like a printed halftone screen. As you scroll, the dot radius grows smoothly from minDot to maxDot, and a layer of larger dots appears around the cursor through a circular mask. Content passed as children sits above the dots.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add halftone-background.en

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

14
4
140
Active edited usageDefault props
<HalftoneBackground/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add https://webberui.com/r/halftone-background.en.json

Or, once registries are configured in components.json, install it as @webberui/halftone-background.

Usage

import { HalftoneBackground } from "@/components/ui/halftone-background";

<HalftoneBackground
  direction="radial"
  className="min-h-[480px] text-neutral-900/15 dark:text-white/15"
>
  <section className="flex min-h-[480px] items-center justify-center">
    <h1 className="text-4xl font-semibold">印刷質感,一路延伸</h1>
  </section>
</HalftoneBackground>

Props

PropTypeDefaultDescription
colorstring"currentColor"Dot color; defaults to currentColor, paired with the root's text-neutral-900/10 dark:text-white/15 so it follows the theme
cellnumber14Halftone cell size (px); one dot per cell
minDotnumber1Dot radius (px) at the start of the scroll range
maxDotnumber4Dot radius (px) at the end of the scroll range; cursor-layer dots are 1.6× this value
direction"top" | "bottom" | "radial""radial"Tone ramp direction: top = dense at top, bottom = dense at bottom, radial = dense in the center
scrollDrivenbooleantrueWhether the dot radius follows scroll progress between minDot and maxDot
cursorbooleantrueWhether enlarged dots appear around the cursor
cursorRadiusnumber140Radius (px) of the enlarged area around the cursor
anglenumber0Rotation of the dot lattice (degrees), like a print screen angle
scrollContainerReact.RefObject<HTMLElement | null>—Optional scroll container; when set, its overall scroll progress (0→1) drives the dot radius, otherwise the component's own position in the viewport is tracked
childrenReact.ReactNode—Content layered above the dots
classNamestring—Passed through to the root container

How it works

  • Pure CSS drawing: the main layer is radial-gradient(circle, var(--wb-halftone-color) calc(var(--wb-halftone-dot) * 1px), transparent …) with a background-size of var(--wb-halftone-cell); no canvas and no images.
  • Scroll driving: useScroll tracks the component from entering the bottom of the viewport to leaving the top (offset: ["start end", "end start"]), maps that to a dot radius, smooths it with a spring (stiffness 120, damping 30) and writes it to --wb-halftone-dot. Pass scrollContainer when it lives inside a fixed-height scroll box.
  • Cursor layer: the same lattice with a dot radius of maxDot × 1.6, revealed only around the cursor by mask-image: radial-gradient(circle var(--wb-halftone-radius) at var(--mx) var(--my), …); pointermove updates the position through a rAF throttle, and the radius springs back to 0 on leave.
  • Screen angle: when angle is not 0, the lattice lives in a child layer whose side is width + height and is rotated, so it covers the box at any angle.
  • Overridable CSS variables: --wb-halftone-color, --wb-halftone-cell (default 14px), --wb-halftone-dot, --wb-halftone-radius (default 140px), --wb-halftone-ease-duration (cursor layer fade, default 400ms).

Accessibility

  • Both dot layers are purely decorative, with aria-hidden="true" and pointer-events-none, so they never get in the way of clicking or reading the content above.
  • On touch devices (pointer: coarse) there is no hover, so the cursor layer is turned off automatically.
  • When the user has reduce motion enabled, the dot radius is pinned at mid density (the average of minDot and maxDot) and no longer reacts to scroll or the cursor.