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.

Installation
Adding any component pulls sketch-provider in as a dependency, but you can install it on its own.
npx shadcn@latest add https://sketchcn.chuwii.com/r/sketch-provider.json
Wrap your app once, at the root
import { 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.

Docs for LLMs
Every component doc is also served as plain markdown, so a coding agent can read it without scraping the page.
Index of every component doc
curl https://sketchcn.chuwii.com/llms.txt
A single component as markdown
curl https://sketchcn.chuwii.com/components/button/md

llms.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.

SketchProvider
Shares the seed with every sketch component below it. Every other drawing option comes from the --sketch-* CSS variables.
NameTypeDefaultCSS variableDescription
childrenReactNode——The tree that can read sketch settings.
seednumber20260828--sketch-seedBase seed for the wobble. Every hook mixes it with its own id, so one seed change reshuffles the whole page deterministically.
Draw the whole page rougher
<SketchProvider seed={1234}>
  <div className="[--sketch-roughness:2] [--sketch-bowing:2]">{children}</div>
</SketchProvider>
Option precedence, lowest to highest

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.

useSketchOutline
Returns a ref and a style for an absolutely positioned svg that traces the parent element.
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.

NameTypeDefaultCSS variableDescription
shape"rectangle" | "underline""rectangle"—Rectangle follows the element border radius; underline draws a single stroke along the bottom edge.
idstringuseId()—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.
opacitynumber——Applied to the returned svg style. Style the svg directly in CSS instead when you want a hover or state transition.
...roughOptionsPartial<Options>—--sketch-*Any RoughJS option except seed: roughness, bowing, stroke, strokeWidth, strokeLineDash, fill, fillStyle, and friends.
useSketchBg
useSketchOutline drawn from the --sketch-bg-* variables, giving a hatched fill and no stroke. Anything it does not define falls back to the shared --sketch-* value.
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>
  );
}
NameTypeDefaultCSS variableDescription
fillcolorcurrentColor--sketch-bg-fillFill colour of the hachure strokes.
fillStylestringhachure--sketch-bg-fill-styleRoughJS fill style used for the shading.
fillWeightnumber0.4--sketch-bg-fill-weightThickness of each hachure stroke.
hachureGapnumber4--sketch-bg-hachure-gapDistance between hachure strokes.
hachureAnglenumber-45--sketch-bg-hachure-angleAngle 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.
strokecolortransparent--sketch-bg-strokeNo outline is drawn, only the fill.
opacitynumber0.5--sketch-bg-opacityKeeps the shading behind the content readable.
Override the hatch angle
<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 });
Live
Hachure
Cross hatch
Zigzag
Flat hatch
Upright hatch
CSS variables
sketch.css ships defaults on :root, and any element can override them for its own outline.
NameTypeDefaultDescription
--sketch-seedquoted 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-roughnessnumber1.1How far strokes stray from the ideal path.
--sketch-bowingnumber1.4How much straight lines bend.
--sketch-strokecolorcurrentColorStroke colour of the outline.
--sketch-stroke-widthnumber1.6Stroke width in pixels.
--sketch-disable-multi-stroke"true" | "false"trueDraw one pass instead of the doubled pencil pass.
--sketch-preserve-vertices"true" | "false"trueKeep corners anchored so shapes stay aligned with the layout box.
--sketch-fillcolor—Fill colour, also used by the CSS fill transition on drawn paths.
--sketch-fill-stylestringsolidRoughJS fill style, such as solid, hachure, or zigzag.
--sketch-fill-weightnumber—Thickness of each fill stroke for hatched fill styles.
--sketch-hachure-gapnumber—Distance between hachure strokes.
--sketch-hachure-anglenumber—Angle of the hachure lines in degrees.
--sketch-dashstringnoneCSS stroke-dasharray on the drawn outline, separate from the RoughJS dashed fill.
--sketch-dash-animationstringnoneCSS animation shorthand for the outline, used by the boil keyframe.
--sketch-fill-opacitynumber1CSS fill-opacity on the drawn path, transitioned over 150ms.
Two values, side by side
--sketch-seed
7
99
--sketch-roughness
0.4
3
--sketch-bowing
0
6
--sketch-stroke
var(--primary)
var(--destructive)
--sketch-stroke-width
1
4
--sketch-disable-multi-stroke
true
false
--sketch-preserve-vertices
true
false
--sketch-fill
var(--primary)
var(--destructive)
--sketch-fill-style
solid
hachure
--sketch-fill-weight
0.5
2.5
--sketch-hachure-gap
3
10
--sketch-hachure-angle
0
90
--sketch-dash
6 4
2 10
--sketch-dash-animation
boil with dash 6 4
boil with dash 5 5
--sketch-fill-opacity
0.2
1
Override on a subtree
<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.

Animations
sketch.css ships four keyframes. They are global, so any element can use them through --sketch-dash-animation, an animation shorthand, or a Tailwind arbitrary animate-[…] value.
NameTypeDefaultCSS variableDescription
sketch-dash-boiloutline dash1s steps(2) infinite--sketch-dash-animationShifts 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-wiggletransform180ms 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-wobbletransform450ms 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-revealmask2400ms 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.
Live, the dashed ones loop and the rest react to hover
sketch-dash-boil
sketch-avatar-wobble
sketch-icon-wiggle
sketch-hatch-reveal
The keyframes, as shipped
@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%;
  }
}
Use them on your own elements
<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" />
Already silenced under prefers-reduced-motion
@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.

Data attributes
sketch.css styles these attributes wherever they appear, so they work on your own markup, not only inside the components.
NameTypeDefaultDescription
data-sketch-outlinesvg—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-bgsvg—Marks the hachure svg and applies --sketch-bg-opacity.
data-sketch-revealancestor—Masks the data-sketch-bg svg below it and runs sketch-hatch-reveal on a loop.
data-paperPaperVariant"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-patterncolor#9c92acColour of the paper texture.
--paper-opacitynumber0.4Opacity of the paper texture layer.
Wipe any hachure svg
<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>
Paper texture behind any element
<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.