Installation
pnpm dlx shadcn@latest add https://dithercn.dev/r/dither-button.jsonUsage
import { DitherButton } from "@/components/dithercn/dither-button";
export function Actions() {
return (
<>
<DitherButton color="blue">Deploy</DitherButton>
<DitherButton variant="outline" fill="dotted">
View logs
</DitherButton>
</>
);
}DitherButton renders a real <button> with type="button" unless you pass another type. A filled button is lit like a pixel-art bevel: the full colour, with a highlight (the colour mixed towards white) dithered down from the top and a shade (mixed towards black) dithered up from the bottom, so the Bayer steps show at rest. Hover fades in a longer, denser highlight. Press fades in a shade from the top, so the face looks pushed in. Only opacity animates, so the cells stay on the grid.
outline buttons are a faint tint inside a hairline edge, with the plain colour dithered up from the bottom. Hover and press make that dither denser and taller, which suits secondary actions.
The label is dark on the bright dark-theme colours and light on the deeper light-theme ones.
Examples
Fills and colours
Every fill in four colours, filled and outline. fill sets the texture of the shade: gradient is a Bayer ramp up from the bottom, dotted a staggered dot lattice, hatched rising diagonals, and solid a thin dithered rim along the bottom edge. The top highlight is the same on all four.
Loading in a form
loading keeps the label and the button's width, steps three cells before the label, pulses the hover highlight, and sets aria-busy. Clicks are ignored until it clears. Submit this one to see it.
As a link
Pass asChild and a single child, such as Next.js Link or a plain <a>, and the child gets the button's look and fill. It stays a link: right click and middle click work, and the router handles navigation.
Props
| Prop | Type | Default |
|---|---|---|
| variant Filled is the lit colour surface. Outline is a faint tint whose dither grows on hover. | Type: "filled" | "outline" | Default "filled" |
| fill Texture of the shade: a Bayer ramp, a dot lattice, diagonals, or a thin rim. | Type: "gradient" | "dotted" | "hatched" | "solid" | Default "gradient" |
| color A palette name such as "blue" or "green", a CSS variable name, or any CSS colour. | Type: DitherColor | Default "ink" |
| size Heights of 32, 36 and 44 pixels. icon is a 36 pixel square for a lone icon; give it an aria-label. | Type: "sm" | "md" | "lg" | "icon" | Default "md" |
| bloom A soft glow in the button's colour that follows the cells. | Type: "off" | "low" | "high" | Default "off" |
| cell Size of one dither cell in CSS pixels. | Type: number | Default 2 |
| loading Busy state: keeps the label, ignores clicks and sets aria-busy. | Type: boolean | Default false |
| asChild Render your own single child, such as a link, with the button's look. | Type: boolean | Default false |
All other props pass through to the <button>, or to your child with asChild.
Notes
- Focus shows a 2 pixel outline with
:focus-visibleand the hover highlight, so keyboard users see the same step as pointer users. - Hover only applies on devices that can hover. Press works everywhere, so touch screens get the pressed-in shade as feedback.
- With reduced motion, the layers switch without fading, the press does not scale, and the loading cells hold still.
- Disabled and loading buttons drop to 45% opacity and a not-allowed or progress cursor. For a disabled link, set
aria-disabledon it and leave outhref. - The fill is plain CSS masks in one shared
<style>tag. The button only needs client JavaScript forasChildandloading.