Components
SketchBorder
A hand-drawn border wrapper for sketch-style layouts.
Rounded Border
A softer rounded border for cards and sections.
Dashed Border
A dashed sketch border for notes or empty states.
Rough Stroke
Increase roughness and stroke width for a stronger sketch.
Code
tsx
181 lines
"use client" import { useEffect, useRef } from "react" import rough from "roughjs" import { cn } from "@/lib/utils" type SketchBorderShape = "rectangle" | "rounded-rectangle" | "circle" | "ellipse" type SketchBorderStyle = "solid" | "dashed" type RoughBorderOptions = { seed?: number stroke?: string strokeWidth?: number fill?: string fillStyle?: "solid" | "hachure" | "zigzag" | "cross-hatch" | "dots" | "dashed" | "zigzag-line" hachureGap?: number hachureAngle?: number roughness?: number bowing?: number } type SketchBorderProps = { children?: React.ReactNode width?: number | string height?: number | string minHeight?: number | string x?: number y?: number shape?: SketchBorderShape borderStyle?: SketchBorderStyle radius?: number borderColor?: string fillColor?: string transparent?: boolean padding?: number rotate?: number roughOptions?: RoughBorderOptions className?: string contentClassName?: string } function roundedRectPath( x: number, y: number, width: number, height: number, radius: number ) { const r = Math.min(radius, width / 2, height / 2) return ` M ${x + r} ${y} L ${x + width - r} ${y} Q ${x + width} ${y} ${x + width} ${y + r} L ${x + width} ${y + height - r} Q ${x + width} ${y + height} ${x + width - r} ${y + height} L ${x + r} ${y + height} Q ${x} ${y + height} ${x} ${y + height - r} L ${x} ${y + r} Q ${x} ${y} ${x + r} ${y} Z ` } export function SketchBorder({ children, width = "100%", height, minHeight = 120, x = 6, y = 6, shape = "rectangle", borderStyle = "solid", radius = 16, borderColor = "#111", fillColor = "transparent", transparent = true, padding = 16, rotate = 0, roughOptions, className, contentClassName, }: SketchBorderProps) { const svgRef = useRef<SVGSVGElement | null>(null) const wrapperRef = useRef<HTMLDivElement | null>(null) useEffect(() => { const svg = svgRef.current const wrapper = wrapperRef.current if (!svg || !wrapper) return const rect = wrapper.getBoundingClientRect() const drawWidth = rect.width - x * 2 const drawHeight = rect.height - y * 2 if (drawWidth <= 0 || drawHeight <= 0) return svg.replaceChildren() const rc = rough.svg(svg) const options = { seed: roughOptions?.seed ?? 99, stroke: roughOptions?.stroke ?? borderColor, strokeWidth: roughOptions?.strokeWidth ?? 1.8, fill: transparent ? undefined : roughOptions?.fill ?? fillColor, fillStyle: transparent ? undefined : roughOptions?.fillStyle ?? "solid", hachureGap: roughOptions?.hachureGap ?? 7, hachureAngle: roughOptions?.hachureAngle ?? -10, roughness: roughOptions?.roughness ?? 1.6, bowing: roughOptions?.bowing ?? 0.9, strokeLineDash: borderStyle === "dashed" ? [8, 8] : undefined, } let node: SVGGElement if (shape === "circle") { const size = Math.min(drawWidth, drawHeight) node = rc.circle(x + drawWidth / 2, y + drawHeight / 2, size, options) } else if (shape === "ellipse") { node = rc.ellipse( x + drawWidth / 2, y + drawHeight / 2, drawWidth, drawHeight, options ) } else if (shape === "rounded-rectangle") { node = rc.path( roundedRectPath(x, y, drawWidth, drawHeight, radius), options ) } else { node = rc.rectangle(x, y, drawWidth, drawHeight, options) } svg.appendChild(node) }, [ x, y, shape, borderStyle, radius, borderColor, fillColor, transparent, roughOptions, children, ]) return ( <div ref={wrapperRef} className={cn("relative", className)} style={{ width, height, minHeight, transform: rotate ? `rotate(${rotate}deg)` : undefined, }} > <svg ref={svgRef} className="pointer-events-none absolute inset-0 h-full w-full" style={{ overflow: "visible" }} aria-hidden="true" /> {children && ( <div className={cn("relative z-10", contentClassName)} style={{ padding }} > {children} </div> )} </div> ) }
Usage
tsx
<SketchBorder borderStyle="dashed" minHeight={280} width={280} transparent={false} fillColor="#FDFBD4" roughOptions={{ fillStyle: "zigzag" }}> <h3 className="text-lg font-semibold">Dashed Border</h3> <p className="mt-2 text-sm text-muted-foreground"> A dashed sketch border for notes or empty states. </p> </SketchBorder>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | - | Content rendered inside the border container. |
width | number | string | "100%" | Sets the width of the border container. |
height | number | string | - | Sets a fixed height for the border container. |
minHeight | number | 120 | Sets the minimum height of the container. |
x | number | 6 | X offset used for RoughJS rendering. |
y | number | 6 | Y offset used for RoughJS rendering. |
shape | SketchBorderShape | "rectangle" | Defines the shape of the border. |
borderStyle | SketchBorderStyle | "solid" | Controls the appearance of the border. |
radius | number | 16 | Corner radius for rounded border shapes. |
borderColor | string | "#111" | Border color of the sketch border. |
fillColor | string | "transparent" | Fill color inside the border. |
transparent | boolean | true | Removes any fill and renders only the border. |
padding | number | 16 | Inner spacing of the content. |
rotate | number | 0 | Rotates the border container in degrees. |
className | string | - | Additional classes applied to the wrapper. |
contentClassName | string | - | Additional classes applied to the content container. |
roughOptions | RoughBorderOptions | - | Custom RoughJS rendering configuration. |
Rough Options
The roughOptions prop allows you to customize the hand-drawn appearance.
seed(number): Used to control the randomness of stroke.stroke(string): It gives the color to the stroke.strokeWidth(number): Sets width of stroke.fill(string): Stes the fill color.fillStyle: Gives the filling style in the component. ["solid","hachure","zigzag","cross-hatch","dots","dashed","zigzag-line"]hachureGap(number): The gap between every hachure stroke.hachureAngle(number): Sets the hachure angle.roughness(number): The amount of roughness, higher value means higher roughness.bowing(number): The bending effect of the stroke.
tsx
<SketchBorder transparent={false} fillColor="pink" minHeight={280} width={280} roughOptions={{ fillStyle: "solid", strokeWidth: 2.4, roughness: 2.4, bowing: 1.2, }} > <h3 className="text-lg font-semibold">Rough Stroke</h3> <p className="mt-2 text-sm text-muted-foreground"> Increase roughness and stroke width for a stronger sketch. </p> </SketchBorder>
Shapes
- rectangle
- rounded-rectangle
- circle
- ellipse
Border Styles
- solid
- dashed