Sketch Provider

Every Sketchcn component draws its outline with RoughJS. SketchProvider holds the shared seed, and the useSketchOutline and useSketchBg hooks turn any element into a hand-drawn one. Adding any component pulls this in as a dependency.

Sketch Provider
The foundation every Sketchcn component draws through.
Provider documentationEvery hook, prop and CSS variable, with live examples.
Read the docs
Seeds
seed 1
seed 42
seed 1234
useSketchOutline
Default
Dashed
Rougher
Underline
useSketchBg
Hachure
Cross hatch
Zigzag

Installation

npx shadcn@latest add https://sketchcn.chuwii.com/r/sketch-provider.json

Usage

Wrap your app once, at the root. The hooks throw when there is no provider above them, so a missing wrapper fails loudly instead of rendering unstyled boxes.

import { SketchProvider } from "@/components/ui/sketch-provider";

export function App({ children }: { children: React.ReactNode }) {
  return <SketchProvider>{children}</SketchProvider>;
}

Everything below it draws through the same seed, so components pick up the hand-drawn outline without any extra wiring.

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { SketchProvider } from "@/components/ui/sketch-provider";
import { Switch } from "@/components/ui/switch";

export function SignUpForm() {
  return (
    <SketchProvider seed={42}>
      <Input placeholder="[email protected]" />
      <Switch defaultChecked />
      <Button>Sign up</Button>
    </SketchProvider>
  );
}

SketchProvider props

PropTypeDefaultDescription
childrenReactNode—The tree that can read sketch settings.
seednumber20260828Base seed for the wobble. Every hook mixes it with its own id, so one seed change reshuffles the whole page deterministically.

Every other drawing option comes from --sketch-* CSS variables.

useSketchOutline

Returns a ref and a style for an absolutely positioned svg that traces the parent element. The parent needs position: relative and isolate so the svg can sit behind the content.

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>
  );
}
OptionTypeDefaultDescription
shape"rectangle" | "circle" | "underline" | "vertical-line""rectangle"Rectangle follows the element border radius; underline and vertical-line draw a single edge stroke.
idstringuseId()Seeds the wobble. Pass a stable id when the geometry must not change.
opacitynumber—Applied to the returned svg style.
...roughOptionsPartial<Options>—Any RoughJS option except seed: roughness, bowing, stroke, strokeWidth, strokeLineDash, fill, fillStyle.

The outline is measured from the padding box and redrawn whenever the element resizes or its class or style attribute changes.

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.

const sketchBg = useSketchBg({ hachureGap: 6 });
OptionDefaultCSS variable
fillcurrentColor--sketch-bg-fill
fillStylehachure--sketch-bg-fill-style
fillWeight0.4--sketch-bg-fill-weight
hachureGap4--sketch-bg-hachure-gap
hachureAngle-45--sketch-bg-hachure-angle
stroketransparent--sketch-bg-stroke
opacity0.5--sketch-bg-opacity

useSketchSeed

Returns the seed of the nearest SketchProvider, for effects drawn outside RoughJS that should reshuffle with it.

const seed = useSketchSeed();

CSS variables

sketch.css ships defaults on :root, and any element can override them for its own outline. They cascade like any other custom property, so Tailwind arbitrary properties, variants and dark mode all work.

<div className="[--sketch-roughness:2.6] [--sketch-bowing:2.4]">
  <Button>Rougher button</Button>
</div>
VariableDefaultDescription
--sketch-seed"20260828"Base seed, overriding the provider for the subtree. Quote it, because CSS minifiers round bare numbers to six significant digits.
--sketch-roughness1.1How far strokes stray from the ideal path.
--sketch-bowing1.4How much straight lines bend.
--sketch-strokecurrentColorStroke colour of the outline.
--sketch-stroke-width1.6Stroke width in pixels.
--sketch-disable-multi-stroketrueDraw one pass instead of the doubled pencil pass.
--sketch-preserve-verticestrueKeep corners anchored to the layout box.
--sketch-fill—Fill colour, also used by the CSS fill transition.
--sketch-fill-stylesolidRoughJS fill style, such as solid, hachure or zigzag.
--sketch-fill-weight—Thickness of each fill stroke for hatched styles.
--sketch-hachure-gap—Distance between hachure strokes.
--sketch-hachure-angle—Angle of the hachure lines in degrees.
--sketch-dashnoneCSS stroke-dasharray on the outline.
--sketch-dash-animationnoneCSS animation shorthand, used by the boil keyframe.
--sketch-fill-opacity1CSS fill-opacity on the drawn path, transitioned over 150ms.

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. They arrive with sketch-provider, so there is nothing to copy into your own CSS.

KeyframeApplied throughUsed by
sketch-dash-boil--sketch-dash + --sketch-dash-animationGhost Button, Input on hover, pressed Toggle
sketch-icon-wiggleanimate-[…] on the iconButton on press, Alert and Toast icons on hover
sketch-avatar-wobble[animation:…] on the elementAvatarGroup on hover
sketch-hatch-revealdata-sketch-reveal on an ancestorSkeleton
@keyframes sketch-dash-boil {
  to {
    stroke-dashoffset: -10;
  }
}

@keyframes sketch-icon-wiggle {
  0%,
  100% {
    transform: rotate(-5deg);
  }

  50% {
    transform: rotate(5deg);
  }
}

/* The lift ends level and raised, so the hovered face stays up while it is held. */
@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 the same way the components do.

<div className="[--sketch-dash:6_4] hover:[--sketch-dash-animation:sketch-dash-boil_1s_steps(2)_infinite]">
  <Button variant="ghost">Boiling outline</Button>
</div>

<BxHeart className="hover:animate-[sketch-icon-wiggle_180ms_ease-in-out_infinite] motion-reduce:animate-none" />

sketch.css already silences the dash, hatch reveal and avatar wobble under prefers-reduced-motion. Animations you add yourself are not covered, so pair them with 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;
  }
}

Data attributes

sketch.css styles these attributes wherever they appear, so they work on your own markup, not only inside the components.

AttributeWhat it does
data-sketch-outlineMarks the outline svg. Its paths take --sketch-dash, --sketch-dash-animation, --sketch-fill and --sketch-fill-opacity, and keep a non-scaling stroke.
data-sketch-bgMarks the hachure svg and applies --sketch-bg-opacity.
data-sketch-revealSet on an ancestor, it masks the data-sketch-bg svg below it and runs sketch-hatch-reveal on a 2400ms loop. This is the whole of Skeleton's animation.
data-paperDraws a repeating paper texture behind the element through a mask. Accepts polkadots, filled-dots, hexagons, graph and plus; default draws nothing.
<div className="relative isolate rounded-lg p-4" data-paper="graph">
  Graph paper behind any element
</div>

<div className="relative isolate" data-sketch-reveal>
  <span>Loading</span>
  <svg aria-hidden="true" data-sketch-bg ref={sketchBg.ref} style={sketchBg.style} />
</div>

The paper layer is an ::before pseudo-element at z-index: -1, so the element needs position: relative and isolate. Two variables tune it.

VariableDefaultDescription
--paper-pattern#9c92acColour of the texture.
--paper-opacity0.4Opacity of the texture layer.

The reveal mask is driven by two registered custom properties, --sketch-bg-reveal-lead and --sketch-bg-reveal-trail. They are declared with @property as percentages so they can be animated; treat them as internal to the reveal.

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.

Examples

Every component inside redraws from the provider seed

seed 42
seed 1234
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { SketchProvider } from "@/components/ui/sketch-provider";
import { Switch } from "@/components/ui/switch";

export function SignUpForm() {
  return (
    <SketchProvider seed={42}>
      <Input placeholder="[email protected]" />
      <Switch defaultChecked />
      <Button>Sign up</Button>
    </SketchProvider>
  );
}