Sketch Provider
Every Sketchcn component draws its outline with RoughJS. SketchProvider holds the drawing settings, and the useSketchOutline and useSketchBg hooks turn any element into a hand-drawn one.
npx shadcn@latest add https://sketchcn.chuwii.com/r/sketch-provider.jsonimport { SketchProvider } from "@/components/ui/sketch-provider";
export function App({ children }: { children: React.ReactNode }) {
return <SketchProvider>{children}</SketchProvider>;
}The hooks throw when there is no provider above them, so a missing wrapper fails loudly instead of rendering unstyled boxes.
curl https://sketchcn.chuwii.com/llms.txtcurl https://sketchcn.chuwii.com/components/button/mdllms.txt lists every component with a link to its markdown, so pointing an agent at that one URL is enough for it to find the rest.
| Name | Type | Default | CSS variable | Description |
|---|---|---|---|---|
| children | ReactNode | — | — | The tree that can read sketch settings. |
| seed | number | 20260828 | --sketch-seed | Base seed for the wobble. Every hook mixes it with its own id, so one seed change reshuffles the whole page deterministically. |
<SketchProvider seed={1234}>
<div className="[--sketch-roughness:2] [--sketch-bowing:2]">{children}</div>
</SketchProvider>CSS variables read from the element, then the options passed to the hook. The seed is the exception. It comes from --sketch-seed when the element sets one, otherwise from the provider, and is always mixed with the hook id.
import { useSketchOutline } from "@/components/ui/sketch-provider";
export function Panel({ children }: { children: React.ReactNode }) {
const sketchOutline = useSketchOutline();
return (
<div className="relative isolate rounded-lg px-4 py-3">
{children}
<svg
aria-hidden="true"
data-sketch-outline
className="-z-10"
ref={sketchOutline.ref}
style={sketchOutline.style}
/>
</div>
);
}The parent needs position: relative and isolate so the svg can sit behind the content. The outline is measured from the padding box and redrawn whenever the element resizes or its class or style attribute changes.
| Name | Type | Default | CSS variable | Description |
|---|---|---|---|---|
| shape | "rectangle" | "underline" | "rectangle" | — | Rectangle follows the element border radius; underline draws a single stroke along the bottom edge. |
| id | string | useId() | — | Seeds the wobble. Because useId() shifts with tree position, pass a stable id when the geometry must not change, such as shared shapes or visual snapshots. |
| opacity | number | — | — | Applied to the returned svg style. Style the svg directly in CSS instead when you want a hover or state transition. |
| ...roughOptions | Partial<Options> | — | --sketch-* | Any RoughJS option except seed: roughness, bowing, stroke, strokeWidth, strokeLineDash, fill, fillStyle, and friends. |
import { useSketchBg } from "@/components/ui/sketch-provider";
export function Highlight({ children }: { children: React.ReactNode }) {
const sketchBg = useSketchBg({ hachureGap: 6 });
return (
<div className="relative isolate rounded-lg px-4 py-3 text-primary">
{children}
<svg
aria-hidden="true"
data-sketch-bg
className="-z-10"
ref={sketchBg.ref}
style={sketchBg.style}
/>
</div>
);
}| Name | Type | Default | CSS variable | Description |
|---|---|---|---|---|
| fill | color | currentColor | --sketch-bg-fill | Fill colour of the hachure strokes. |
| fillStyle | string | hachure | --sketch-bg-fill-style | RoughJS fill style used for the shading. |
| fillWeight | number | 0.4 | --sketch-bg-fill-weight | Thickness of each hachure stroke. |
| hachureGap | number | 4 | --sketch-bg-hachure-gap | Distance between hachure strokes. |
| hachureAngle | number | -45 | --sketch-bg-hachure-angle | Angle of the hachure strokes in degrees, measured clockwise from the x axis. Components that wipe their shading, such as Skeleton, read the same value to keep the reveal edge parallel to the strokes. |
| stroke | color | transparent | --sketch-bg-stroke | No outline is drawn, only the fill. |
| opacity | number | 0.5 | --sketch-bg-opacity | Keeps the shading behind the content readable. |
<div className="[--sketch-bg-hachure-angle:0]">
<Skeleton className="h-24 w-full" />
</div>
// or per instance, without touching the cascade
const sketchBg = useSketchBg({ hachureAngle: 0 });| Name | Type | Default | Description |
|---|---|---|---|
| --sketch-seed | quoted number | "20260828" | Base seed for the wobble, mixed with each instance id, overriding the SketchProvider seed for the subtree. Quote it, because CSS minifiers round bare numbers to six significant digits. |
| --sketch-roughness | number | 1.1 | How far strokes stray from the ideal path. |
| --sketch-bowing | number | 1.4 | How much straight lines bend. |
| --sketch-stroke | color | currentColor | Stroke colour of the outline. |
| --sketch-stroke-width | number | 1.6 | Stroke width in pixels. |
| --sketch-disable-multi-stroke | "true" | "false" | true | Draw one pass instead of the doubled pencil pass. |
| --sketch-preserve-vertices | "true" | "false" | true | Keep corners anchored so shapes stay aligned with the layout box. |
| --sketch-fill | color | — | Fill colour, also used by the CSS fill transition on drawn paths. |
| --sketch-fill-style | string | solid | RoughJS fill style, such as solid, hachure, or zigzag. |
| --sketch-fill-weight | number | — | Thickness of each fill stroke for hatched fill styles. |
| --sketch-hachure-gap | number | — | Distance between hachure strokes. |
| --sketch-hachure-angle | number | — | Angle of the hachure lines in degrees. |
| --sketch-dash | string | none | CSS stroke-dasharray on the drawn outline, separate from the RoughJS dashed fill. |
| --sketch-dash-animation | string | none | CSS animation shorthand for the outline, used by the boil keyframe. |
| --sketch-fill-opacity | number | 1 | CSS fill-opacity on the drawn path, transitioned over 150ms. |
<div className="[--sketch-roughness:2.6] [--sketch-bowing:2.4]">
<Button>Rougher button</Button>
</div>Variables are read from the drawn element itself, so they cascade like any other CSS custom property and work with Tailwind arbitrary properties, variants, and dark mode. Read sketch.css in full.
| Name | Type | Default | CSS variable | Description |
|---|---|---|---|---|
| sketch-dash-boil | outline dash | 1s steps(2) infinite | --sketch-dash-animation | Shifts the dash offset by one full period in two steps, so the outline reads as redrawn by hand. Needs --sketch-dash set. With a 6 4 dash the outline jitters; with 5 5 the dashes and gaps swap places. Used by the ghost Button, the Input file selector on hover, and Toggle while pressed. |
| sketch-icon-wiggle | transform | 180ms ease-in-out infinite | — | Rocks an icon between -5 and 5 degrees. Used by Button on press and by the Alert and Toast icons on hover. |
| sketch-avatar-wobble | transform | 450ms ease-out forwards | — | Lifts an element out of a stack and lands it level but raised, so it stays up while hovered. Used by AvatarGroup. |
| sketch-hatch-reveal | mask | 2400ms linear infinite | — | Wipes the hachure mask on and off along the hatch angle. Applied by data-sketch-reveal, which is the whole of Skeleton's animation. |
@keyframes sketch-dash-boil {
to {
stroke-dashoffset: -10;
}
}
@keyframes sketch-icon-wiggle {
0%,
100% {
transform: rotate(-5deg);
}
50% {
transform: rotate(5deg);
}
}
@keyframes sketch-avatar-wobble {
0% {
transform: translateY(0) rotate(0deg);
}
35% {
transform: translateY(-5px) rotate(-5deg);
}
60% {
transform: translateY(-2px) rotate(4deg);
}
80% {
transform: translateY(-4px) rotate(-2deg);
}
100% {
transform: translateY(-3px) rotate(0deg);
}
}
@keyframes sketch-hatch-reveal {
0% {
--sketch-bg-reveal-lead: 0%;
--sketch-bg-reveal-trail: 0%;
}
45%,
55% {
--sketch-bg-reveal-lead: 100%;
--sketch-bg-reveal-trail: 0%;
}
100% {
--sketch-bg-reveal-lead: 100%;
--sketch-bg-reveal-trail: 100%;
}
}<div className="[--sketch-dash:6_4] hover:[--sketch-dash-animation:sketch-dash-boil_1s_steps(2)_infinite]">
<Button variant="ghost">Boiling outline</Button>
</div>
<Info className="hover:animate-[sketch-icon-wiggle_180ms_ease-in-out_infinite] motion-reduce:animate-none" />@media (prefers-reduced-motion: reduce) {
[data-sketch-outline] path {
animation: none;
}
[data-sketch-reveal] [data-sketch-bg] {
animation: none;
mask-image: none;
-webkit-mask-image: none;
}
[data-slot="avatar-group"] > [data-slot="avatar"]:hover {
animation: none;
}
}Animations you add yourself are not covered by that block, so pair them with motion-reduce:animate-none.
| Name | Type | Default | Description |
|---|---|---|---|
| data-sketch-outline | svg | — | Marks the outline svg. Its paths keep a non-scaling stroke and read --sketch-dash, --sketch-dash-animation, --sketch-fill and --sketch-fill-opacity. |
| data-sketch-bg | svg | — | Marks the hachure svg and applies --sketch-bg-opacity. |
| data-sketch-reveal | ancestor | — | Masks the data-sketch-bg svg below it and runs sketch-hatch-reveal on a loop. |
| data-paper | PaperVariant | "default" | Draws a repeating paper texture on a ::before at z-index -1, so the element needs position: relative and isolate. Accepts polkadots, filled-dots, hexagons, graph and plus. |
| --paper-pattern | color | #9c92ac | Colour of the paper texture. |
| --paper-opacity | number | 0.4 | Opacity of the paper texture layer. |
<div className="relative isolate rounded-lg px-4 py-3" data-sketch-reveal>
<span>Loading</span>
<svg
aria-hidden="true"
data-sketch-bg
className="-z-10"
ref={sketchBg.ref}
style={sketchBg.style}
/>
</div><div
className="relative isolate rounded-lg p-4 [--paper-opacity:0.5]"
data-paper="graph"
>
Graph paper behind any element
</div>The reveal mask is driven by --sketch-bg-reveal-lead and --sketch-bg-reveal-trail, declared with @property so they can be animated. Treat them as internal to the reveal; the angle follows --sketch-bg-hachure-angle.