Texture
betaSets how the parts inside it draw their color, like charts, meters, and buttons: dots, a gradient, or grain, with a soft glow on dark pages.
Preview
dither
Monthly recurring revenue
$48,210
gradient
Monthly recurring revenue
$48,210
grain
Monthly recurring revenue
$48,210
flat
Monthly recurring revenue
$48,210
texture
keep flat
choices
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. Add the component.
npx shadcn@latest add @txbrendan/dither
Use it for
- A dashboard, a status page, or a landing page that wants a texture on its charts and meters.
- Wrap a card, a section, or the whole app in Texture. Every part inside that has a texture prop draws its color that way. Dither is dots, gradient fades from the full color to a faint wash, and grain is fine speckle.
- Hover a gradient or grain part to answer the pointer: gradient's full color runs down the part, and grain fills in to solid. With reduced motion, gradient fades in place.
- Pass texture on one part to change just that part, with no wrapper.
- Pass texture="flat" on a part, or on an inner Texture, to keep it flat inside a Texture.
- Dither is the old name for Texture, and it still works.
- Pass fill, start, line, intro, hover, or idle to set how every Area Chart inside lays out and moves its dots. Each sets a --dither- variable, like --dither-intro, so the same choice can sit on <body> in CSS. Bar Chart reads fill, intro, hover, and idle. Its bars have no line or start.
- Parts that take texture: Sparkline, Progress, Ring Progress, Button, Avatar, and Slideshow take all four. Bar Chart and Area Chart take flat and dither, and draw flat under gradient or grain.
- A change of texture fades one into the other where it sits, over --motion-duration-slow. A box that moves --dither itself, like a card that cycles between flat and dots, sets --dither-fade: 0s on itself, so the parts inside follow it with no second fade.
- A chart of your own. Put DitherBoxMask in its SVG defs with the plot's box, and mask the filled shapes with it. The dots snap to the plot and thin toward its bottom. Pass thin "none" for even dots, thin "dense" for dots solid at the top that stay dense longer, or a box's from with thin "from" to thin from a bar's own top. Dither Motion moves them.
Avoid it for
- Text people must read. Textures thin out the shapes, so keep body text, labels, and numbers flat.
- Saying something only with a texture. It is a look, and the part still says its value in text or its name.
- A dense table or form. The texture adds noise where people need to scan.
- An animation class of your own on a dithered part, like a pulse while it loads. The dots' intro sets that part's animation, so one of the two is lost. Put yours on a wrapper around the part.
Props
- texture
The texture every part inside draws. "flat" turns it off again inside another Texture.
"flat" | "dither" | "gradient" | "grain" · default "dither"
- fill
How the dots in every Area Chart and Bar Chart inside thin toward the bottom: one by one, or in 8 even steps. Its own fill wins.
"scatter" | "steps"
- start
How every Area Chart inside starts: fading in from the axis, or 16 px in from it. Its own start wins.
"fade" | "gap"
- line
How each Area Chart line inside starts: fading in, or breaking into dots. Its own line wins.
"fade" | "dots"
- intro
How the dots in every Area Chart and Bar Chart inside come in. Its own intro wins.
"fade" | "sweep" | "rain" | "ripple" | "pop" | "fall"
- hover
Whether hidden dots fill in near the pointer, in every Area Chart and Bar Chart inside. Its own hover wins.
"fill" | "none"
- idle
What the dots in every Area Chart and Bar Chart inside do at rest. Its own idle wins.
"none" | "wave" | "twinkle" | "breathe" | "drift"
- children
The parts that take the texture.
ReactNode
- className
Extra classes. Texture draws no box of its own, since it is display: contents.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| texture | "flat" | "dither" | "gradient" | "grain" | "dither" | The texture every part inside draws. "flat" turns it off again inside another Texture. |
| fill | "scatter" | "steps" | - | How the dots in every Area Chart and Bar Chart inside thin toward the bottom: one by one, or in 8 even steps. Its own fill wins. |
| start | "fade" | "gap" | - | How every Area Chart inside starts: fading in from the axis, or 16 px in from it. Its own start wins. |
| line | "fade" | "dots" | - | How each Area Chart line inside starts: fading in, or breaking into dots. Its own line wins. |
| intro | "fade" | "sweep" | "rain" | "ripple" | "pop" | "fall" | - | How the dots in every Area Chart and Bar Chart inside come in. Its own intro wins. |
| hover | "fill" | "none" | - | Whether hidden dots fill in near the pointer, in every Area Chart and Bar Chart inside. Its own hover wins. |
| idle | "none" | "wave" | "twinkle" | "breathe" | "drift" | - | What the dots in every Area Chart and Bar Chart inside do at rest. Its own idle wins. |
| children | ReactNode | - | The parts that take the texture. |
| className | string | - | Extra classes. Texture draws no box of its own, since it is display: contents. |
Accessibility
- Texture changes only how color is drawn. Names, roles, values, and keyboard behavior stay the same.
- Texture adds no box, so it never changes layout, focus order, or what a screen reader hears.
- A textured shape keeps a faint wash of its color under the dots, gradient, or grain, so it still reads as one piece.
- Hover only runs on screens that can hover, so a tap never leaves it stuck. It is a fade, and with reduced motion gradient fades in place instead of running down.
- The glow shows only on dark pages, through --elevation-glow, which is 0px in light.
- When a dithered part first shows, its dots come in one at a time, over 40% of --motion-pulse-duration, and only once. A part further down the page waits until it starts to come on screen, then plays at the same pace however fast it was scrolled to. Button and Avatar wait with CSS alone, in browsers that can tie an animation to scrolling, and play on mount in the rest.
- Button and Avatar follow their nearest scrolling box, and a box with overflow hidden or auto counts as one even when it cannot scroll. Inside such a box, their dots wait for the box to scroll them into view, and play on mount when they already sit in its view, even while the box itself is off screen. A box with overflow-clip still clips but is not a scrolling box, so parts inside it wait for the page.
- With reduced motion or motion off, the pulse is 0s, so every dot shows at once. A print shows every dot.
- A change of texture is a fade, so reduced motion keeps it. With motion off it swaps at once.
Examples
A dithered dashboard card
<Texture>
<BarChart
title="Deploys to production"
data={[{ week: "W1", deploys: 52 }, { week: "W2", deploys: 61 }, { week: "W3", deploys: 74 }]}
config={{ deploys: { label: "Deploys" } }}
categoryKey="week"
/>
<Progress value={28} tone="brand" label="Compiling 214 modules" showValue />
</Texture>Gradient meters
<Texture texture="gradient">
<RingProgress value={68} tone="brand" aria-label="CPU load" />
<Progress value={28} tone="brand" label="Compiling 214 modules" showValue />
</Texture>One part, no wrapper
<Button tone="brand" texture="grain">Save changes</Button>Keep one part flat
<Texture>
<RingProgress value={68} tone="brand" aria-label="CPU load" />
<RingProgress value={40} texture="flat" aria-label="Memory" />
</Texture>