React Sketch Canvas
Guides

Colors & theming

Control stroke and canvas color, including dark mode support.

The canvas takes color values as plain props. There is no built-in color picker. Pass strokeColor and canvasColor from your own state and the canvas re-renders when they change.

Stroke and canvas color

Use strokeColor for the pen color and canvasColor for the background fill. Both props accept any standard CSS color format, including:

  • Hexadecimal: #a855f7 or #000
  • Named colors: red, transparent, or currentColor
  • Functional notations: rgb(168 85 247), rgba(0, 0, 0, 0.5), or hsl(270 91% 65%)
  • CSS Variables: var(--stroke-color)
Configuring colors
export function () {
  return (
    <
      ="#a855f7" // Hex code
      ="rgba(255, 255, 255, 0.9)" // Functional notation
    />
  );
}
Stroke Color
Canvas Color
Drafting Presets
App.tsx
import { Layers, Paintbrush, Palette } from "lucide-react";import { type ChangeEvent, useRef, useState } from "react";import {	ReactSketchCanvas,	type ReactSketchCanvasRef,} from "react-sketch-canvas";interface ColorPreset {	name: string;	canvas: string;	stroke: string;}const PRESETS: ColorPreset[] = [	{ name: "Classic Ink", canvas: "#ffffff", stroke: "#0f172a" },	{ name: "Pine Forest", canvas: "#f4f8f6", stroke: "#106358" },	{ name: "Blueprint", canvas: "#0b1c33", stroke: "#60a5fa" },	{ name: "Terminal", canvas: "#090d16", stroke: "#10b981" },];export default function App() {	const canvasRef = useRef<ReactSketchCanvasRef>(null);	const [strokeColor, setStrokeColor] = useState("#106358");	const [canvasColor, setCanvasColor] = useState("#f4f8f6");	const handleStrokeColorChange = (event: ChangeEvent<HTMLInputElement>) => {		setStrokeColor(event.target.value);	};	const handleCanvasColorChange = (event: ChangeEvent<HTMLInputElement>) => {		setCanvasColor(event.target.value);	};	const applyPreset = (preset: ColorPreset) => {		setCanvasColor(preset.canvas);		setStrokeColor(preset.stroke);	};	return (		<div className="not-prose flex flex-col gap-4 w-full">			{/* Color Options Bar */}			<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 p-4 rounded-lg border border-fd-border bg-fd-card shadow-sm text-fd-foreground">				{/* Custom Color Controls */}				<div className="flex flex-wrap gap-4 items-center">					{/* Stroke Color */}					<div className="flex flex-col gap-1.5">						<span className="flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wider text-fd-muted-foreground">							<Paintbrush className="w-3.5 h-3.5" />							Stroke Color						</span>						<label className="relative flex items-center gap-2 px-3 py-1.5 rounded-md border border-fd-border bg-fd-muted hover:bg-fd-accent/50 cursor-pointer transition-colors duration-200 text-xs font-medium">							<span								className="w-4 h-4 rounded-full border border-fd-border shadow-inner"								style={{ backgroundColor: strokeColor }}							/>							<span className="font-mono text-[11px]">								{strokeColor.toUpperCase()}							</span>							<input								type="color"								value={strokeColor}								onChange={handleStrokeColorChange}								className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"							/>						</label>					</div>					{/* Canvas Color */}					<div className="flex flex-col gap-1.5">						<span className="flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wider text-fd-muted-foreground">							<Layers className="w-3.5 h-3.5" />							Canvas Color						</span>						<label className="relative flex items-center gap-2 px-3 py-1.5 rounded-md border border-fd-border bg-fd-muted hover:bg-fd-accent/50 cursor-pointer transition-colors duration-200 text-xs font-medium">							<span								className="w-4 h-4 rounded-full border border-fd-border shadow-inner"								style={{ backgroundColor: canvasColor }}							/>							<span className="font-mono text-[11px]">								{canvasColor.toUpperCase()}							</span>							<input								type="color"								value={canvasColor}								onChange={handleCanvasColorChange}								className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"							/>						</label>					</div>				</div>				{/* Presets Grid */}				<div className="flex flex-col gap-1.5 flex-1 max-w-md">					<span className="flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wider text-fd-muted-foreground">						<Palette className="w-3.5 h-3.5" />						Drafting Presets					</span>					<div className="flex flex-wrap gap-2">						{PRESETS.map((preset) => {							const isActive =								canvasColor.toLowerCase() === preset.canvas.toLowerCase() &&								strokeColor.toLowerCase() === preset.stroke.toLowerCase();							return (								<button									key={preset.name}									type="button"									onClick={() => applyPreset(preset)}									className={`flex items-center gap-1.5 px-2.5 py-1 rounded border text-xs font-medium transition-all duration-200 ${										isActive											? "border-fd-primary bg-fd-primary/10 text-fd-primary"											: "border-fd-border bg-fd-card text-fd-muted-foreground hover:text-fd-foreground hover:bg-fd-accent/50"									}`}								>									{/* preset color swatch mini */}									<div className="flex -space-x-1.5">										<div											className="w-2.5 h-2.5 rounded-full border border-fd-border shadow-inner z-10"											style={{ backgroundColor: preset.stroke }}										/>										<div											className="w-2.5 h-2.5 rounded-full border border-fd-border shadow-inner"											style={{ backgroundColor: preset.canvas }}										/>									</div>									{preset.name}								</button>							);						})}					</div>				</div>			</div>			{/* Drawing Workspace */}			<div className="relative overflow-hidden rounded-lg border border-fd-border aspect-video min-h-[240px] shadow-sm transition-colors duration-200">				<ReactSketchCanvas					ref={canvasRef}					strokeColor={strokeColor}					canvasColor={canvasColor}				/>			</div>		</div>	);}

This pattern works well when color is part of a larger form, theme editor, or annotation workflow.

Individual strokes vs. bulk recoloring

strokeColor is captured at the moment each stroke is drawn, meaning individual strokes can be colored differently. If you change strokeColor in your state between strokes, the user can draw a multi-colored sketch:

  • Per-stroke colors: Changing strokeColor only affects new strokes. Past strokes remain in the color they were originally drawn with.
  • Bulk recoloring: If you want to recolor all existing and future strokes simultaneously (for example, when switching between dark and light mode), pass a CSS Variable (e.g., strokeColor="var(--stroke-color)") or currentColor. When the CSS variable or color changes in your stylesheet, all strokes using that variable will immediately update to the new color.

To use bulk recoloring with CSS variables, make sure the variables are defined on a parent container of the canvas and updated when your application theme shifts.

Dark and light mode

The canvas does not switch colors automatically when your app changes theme. Pass values that reflect your app's theme state, or define CSS variables near the canvas and update those variables from your app's theme selector.

Active Tool
Stroke Width5px
Eraser Width10px
Eraser Mode
App.tsx
import { Eraser, Pencil, Settings2 } from "lucide-react";import { type ChangeEvent, useRef, useState } from "react";import {	type EraserMode,	ReactSketchCanvas,	type ReactSketchCanvasRef,} from "react-sketch-canvas";export default function App() {	const canvasRef = useRef<ReactSketchCanvasRef>(null);	const [eraseMode, setEraseMode] = useState(false);	const [eraserMode, setEraserMode] = useState<EraserMode>("mask");	const [strokeWidth, setStrokeWidth] = useState(5);	const [eraserWidth, setEraserWidth] = useState(10);	const handlePenClick = () => {		setEraseMode(false);		canvasRef.current?.eraseMode(false);	};	const handleEraserClick = () => {		setEraseMode(true);		canvasRef.current?.eraseMode(true);	};	const handleStrokeWidthChange = (event: ChangeEvent<HTMLInputElement>) => {		setStrokeWidth(+event.target.value);	};	const handleEraserWidthChange = (event: ChangeEvent<HTMLInputElement>) => {		setEraserWidth(+event.target.value);	};	const handleEraserModeChange = (mode: EraserMode) => {		if (!eraseMode) {			return;		}		setEraserMode(mode);	};	return (		<div className="not-prose flex flex-col gap-4 w-full">			{/* Tools Panel */}			<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 p-4 rounded-lg border border-fd-border bg-fd-card shadow-sm text-fd-foreground">				{/* Active Tool */}				<div className="inline-flex flex-col gap-2">					<span className="text-xs font-semibold uppercase tracking-wider text-fd-muted-foreground">						Active Tool					</span>					<div className="inline-flex rounded-md p-1 bg-fd-muted border border-fd-border w-fit">						<button							type="button"							onClick={handlePenClick}							className={`inline-flex items-center gap-2 px-3 py-1.5 rounded-md text-xs font-medium transition-all duration-200 ${								!eraseMode									? "bg-fd-primary text-fd-primary-foreground shadow-sm"									: "text-fd-muted-foreground hover:text-fd-foreground hover:bg-fd-accent/50"							}`}						>							<Pencil className="w-3.5 h-3.5" />							Pen						</button>						<button							type="button"							onClick={handleEraserClick}							className={`inline-flex items-center gap-2 px-3 py-1.5 rounded-md text-xs font-medium transition-all duration-200 ${								eraseMode									? "bg-fd-primary text-fd-primary-foreground shadow-sm"									: "text-fd-muted-foreground hover:text-fd-foreground hover:bg-fd-accent/50"							}`}						>							<Eraser className="w-3.5 h-3.5" />							Eraser						</button>					</div>				</div>				{/* Sliders */}				<div className="flex flex-1 flex-col sm:flex-row gap-4">					{/* Stroke Width Slider */}					<div						className={`flex flex-col flex-1 gap-2 transition-opacity duration-200 ${eraseMode ? "opacity-40" : "opacity-100"}`}					>						<div className="flex justify-between items-center text-xs font-semibold uppercase tracking-wider text-fd-muted-foreground">							<span htmlFor="strokeWidth">Stroke Width</span>							<span className="font-mono text-fd-foreground bg-fd-muted border border-fd-border px-1.5 py-0.5 rounded text-[10px]">								{strokeWidth}px							</span>						</div>						<input							disabled={eraseMode}							type="range"							min="1"							max="20"							step="1"							id="strokeWidth"							value={strokeWidth}							onChange={handleStrokeWidthChange}							className="w-full accent-fd-primary cursor-pointer disabled:cursor-not-allowed"						/>					</div>					{/* Eraser Width Slider */}					<div						className={`flex flex-col flex-1 gap-2 transition-opacity duration-200 ${!eraseMode ? "opacity-40" : "opacity-100"}`}					>						<div className="flex justify-between items-center text-xs font-semibold uppercase tracking-wider text-fd-muted-foreground">							<span htmlFor="eraserWidth">Eraser Width</span>							<span className="font-mono text-fd-foreground bg-fd-muted border border-fd-border px-1.5 py-0.5 rounded text-[10px]">								{eraserWidth}px							</span>						</div>						<input							disabled={!eraseMode}							type="range"							min="1"							max="20"							step="1"							id="eraserWidth"							value={eraserWidth}							onChange={handleEraserWidthChange}							className="w-full accent-fd-primary cursor-pointer disabled:cursor-not-allowed"						/>					</div>				</div>				{/* Eraser Mode */}				<div className="inline-flex flex-col gap-2">					<span className="flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wider text-fd-muted-foreground">						<Settings2 className="w-3.5 h-3.5" />						Eraser Mode					</span>					<div className="inline-flex w-fit rounded-md border border-fd-border bg-fd-muted p-1">						<button							type="button"							disabled={!eraseMode}							aria-pressed={eraserMode === "mask"}							onClick={() => handleEraserModeChange("mask")}							className="inline-flex items-center rounded-md px-2 py-1.5 text-xs font-medium transition-all duration-200 hover:bg-fd-accent aria-pressed:bg-fd-primary aria-pressed:text-fd-primary-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fd-primary disabled:cursor-not-allowed disabled:opacity-45 disabled:hover:bg-transparent"						>							Mask						</button>						<button							type="button"							disabled={!eraseMode}							aria-pressed={eraserMode === "stroke"}							onClick={() => handleEraserModeChange("stroke")}							className="inline-flex items-center rounded-md px-2 py-1.5 text-xs font-medium transition-all duration-200 hover:bg-fd-accent aria-pressed:bg-fd-primary aria-pressed:text-fd-primary-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fd-primary disabled:cursor-not-allowed disabled:opacity-45 disabled:hover:bg-transparent"						>							Stroke						</button>					</div>				</div>			</div>			{/* Canvas Workspace */}			<div className="canvas-wrapper relative overflow-hidden rounded-lg border border-fd-border aspect-video min-h-[240px] shadow-sm">				<style>					{`						.canvas-wrapper {							--rsc-theme-canvas: white;							--rsc-theme-stroke: blue;						}						.dark .canvas-wrapper {							--rsc-theme-canvas: black;							--rsc-theme-stroke: red;						}					`}				</style>				<ReactSketchCanvas					ref={canvasRef}					canvasColor="var(--rsc-theme-canvas)"					strokeColor="var(--rsc-theme-stroke)"					eraserMode={eraserMode}					strokeWidth={strokeWidth}					eraserWidth={eraserWidth}				/>			</div>		</div>	);}

The example keeps the controls styled by the documentation site, then scopes canvas-only variables on .canvas-wrapper. The .dark .canvas-wrapper selector updates only the values passed to canvasColor and strokeColor.

CSS variables in strokeColor and canvasColor resolve when the canvas renders. If your theme changes without re-rendering the React component, trigger a parent re-render so the canvas reads the updated variable values.

On this page