Components
Separator
A hand-drawn separator or divider component with lots of customisation freedom.
Code
tsx
296 lines
"use client" import { useEffect, useRef } from "react" import rough from "roughjs" import { cn } from "@/lib/utils" type SeperatorVariant = "dashed" | "curly" | "wavy" | "straight" type SeperatorOrientation = "horizontal" | "vertical" type SeperatorColor = "black" | "yellow" | "purple" | "green" | "pink" | "blue" | "gray" | string type RoughSeperatorOptions = { seed?: number strokeWidth?: number roughness?: number bowing?: number dashGap?: number dashLength?: number } type SeperatorProps = { variant?: SeperatorVariant orientation?: SeperatorOrientation length?: number | string thickness?: number color?: SeperatorColor opacity?: number /** - Controls wave/curly intensity. */ amplitude?: number /** - Controls how many waves/curls appear. */ frequency?: number rotate?: number className?: string roughOptions?: RoughSeperatorOptions } const colorMap: Record<string, string> = { black: "#111111", yellow: "#fde047", purple: "#c084fc", green: "#86efac", pink: "#f9a8d4", blue: "#93c5fd", gray: "#9ca3af", } function getColor(color: SeperatorColor) { return colorMap[color] ?? color } function makeHorizontalWavyPath( width: number, centerY: number, amplitude: number, frequency: number ) { const segments = Math.max(2, frequency) const step = width / segments let path = `M 0 ${centerY}` for (let i = 0; i < segments; i++) { const x1 = i * step + step / 2 const x2 = (i + 1) * step const y1 = i % 2 === 0 ? centerY - amplitude : centerY + amplitude const y2 = centerY path += ` Q ${x1} ${y1} ${x2} ${y2}` } return path } function makeVerticalWavyPath( height: number, centerX: number, amplitude: number, frequency: number ) { const segments = Math.max(2, frequency) const step = height / segments let path = `M ${centerX} 0` for (let i = 0; i < segments; i++) { const y1 = i * step + step / 2 const y2 = (i + 1) * step const x1 = i % 2 === 0 ? centerX - amplitude : centerX + amplitude const x2 = centerX path += ` Q ${x1} ${y1} ${x2} ${y2}` } return path } function makeHorizontalCurlyPath( width: number, centerY: number, amplitude: number, frequency: number ) { const curls = Math.max(1, frequency) const step = width / curls let path = `M 0 ${centerY}` for (let i = 0; i < curls; i++) { const start = i * step const mid = start + step / 2 const end = start + step path += ` C ${start + step * 0.15} ${centerY - amplitude}, ${mid - step * 0.15} ${centerY - amplitude}, ${mid} ${centerY} C ${mid + step * 0.15} ${centerY + amplitude}, ${end - step * 0.15} ${centerY + amplitude}, ${end} ${centerY} ` } return path } function makeVerticalCurlyPath( height: number, centerX: number, amplitude: number, frequency: number ) { const curls = Math.max(1, frequency) const step = height / curls let path = `M ${centerX} 0` for (let i = 0; i < curls; i++) { const start = i * step const mid = start + step / 2 const end = start + step path += ` C ${centerX - amplitude} ${start + step * 0.15}, ${centerX - amplitude} ${mid - step * 0.15}, ${centerX} ${mid} C ${centerX + amplitude} ${mid + step * 0.15}, ${centerX + amplitude} ${end - step * 0.15}, ${centerX} ${end} ` } return path } export function Seperator({ variant = "wavy", orientation = "horizontal", length = "100%", thickness = 28, color = "black", opacity = 1, amplitude = 6, frequency = 8, rotate = 0, className, roughOptions, }: SeperatorProps) { 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 width = orientation === "horizontal" ? rect.width : thickness const height = orientation === "horizontal" ? thickness : rect.height if (width <= 0 || height <= 0) return svg.replaceChildren() const rc = rough.svg(svg) const stroke = getColor(color) const strokeWidth = roughOptions?.strokeWidth ?? 2.2 const centerX = width / 2 const centerY = height / 2 let node: SVGGElement if (variant === "straight" || variant === "dashed") { const x1 = orientation === "horizontal" ? 0 : centerX const y1 = orientation === "horizontal" ? centerY : 0 const x2 = orientation === "horizontal" ? width : centerX const y2 = orientation === "horizontal" ? centerY : height node = rc.line(x1, y1, x2, y2, { seed: roughOptions?.seed ?? 33, stroke, strokeWidth, roughness: roughOptions?.roughness ?? 1.4, bowing: roughOptions?.bowing ?? 0.8, strokeLineDash: variant === "dashed" ? [ roughOptions?.dashLength ?? 10, roughOptions?.dashGap ?? 8, ] : undefined, }) } else { const path = variant === "wavy" ? orientation === "horizontal" ? makeHorizontalWavyPath( width, centerY, amplitude, frequency ) : makeVerticalWavyPath( height, centerX, amplitude, frequency ) : orientation === "horizontal" ? makeHorizontalCurlyPath( width, centerY, amplitude, frequency ) : makeVerticalCurlyPath( height, centerX, amplitude, frequency ) node = rc.path(path, { seed: roughOptions?.seed ?? 44, stroke, strokeWidth, roughness: roughOptions?.roughness ?? 1.5, bowing: roughOptions?.bowing ?? 0.9, fill: "none", }) } node.style.opacity = String(opacity) svg.appendChild(node) }, [ variant, orientation, thickness, color, opacity, amplitude, frequency, roughOptions, ]) return ( <div ref={wrapperRef} className={cn("relative shrink-0", className)} style={{ width: orientation === "horizontal" ? length : thickness, height: orientation === "horizontal" ? thickness : length, rotate: `${rotate}deg`, }} aria-hidden="true" > <svg ref={svgRef} className="absolute inset-0 h-full w-full overflow-visible" /> </div> ) }
Usage
tsx
<Seperator variant="dashed" color="purple" roughOptions={{ strokeWidth: 2, roughness: 1, }} />
Props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | SeparatorVariant | "wavy" | Controls the separator style. |
orientation | SeparatorOrientation | "horizontal" | Sets the separator direction. |
length | number | string | "100%" | Length of the separator. |
thickness | number | 28 | Thickness of the separator drawing area. |
color | SeparatorColor | "black" | Color of the separator line. |
opacity | number | 1 | Opacity of the separator. |
amplitude | number | 6 | Controls the height/intensity of waves and curves. |
frequency | number | 8 | Controls the number of waves or repetitions. |
rotate | number | 0 | Rotates the separator in degrees. |
className | string | - | Additional classes applied to the wrapper. |
roughOptions | RoughSeparatorOptions | - | 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
<Seperator variant="curly" color="orange" amplitude={10} frequency={6} roughOptions={{ strokeWidth: 2.5, roughness: 1.2, }} />
Variants
- wavy
- dashed
- zigzag
- curly
- straight
Orientations
- horizontal
- vertical
Colors
- black
- purple
- green
- pink
- blue
- yellow
- gray