Components
Tape
A torn tape component for sticking paper effects.
Code
tsx
193 lines
"use client" import { useEffect, useRef } from "react" import rough from "roughjs" import { cn } from "@/lib/utils" type TapeVariant = "yellow" | "pink" | "green" | "blue" | "purple" | "orange" | "gray" type TapeStyle = "smooth" | "torn" | "side-torn" type RoughTapeOptions = { 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 TapeProps = { width?: number height?: number variant?: TapeVariant tapeStyle?: TapeStyle rotate?: number opacity?: number roughOptions?: RoughTapeOptions className?: string } const colors: Record<TapeVariant, string> = { yellow: "#fde047", pink: "#f9a8d4", green: "#86efac", blue: "#93c5fd", purple: "#c084fc", orange: "#fdba74", gray: "#d1d5db", } const seedMap: Record<TapeVariant, number> = { yellow: 20, pink: 40, green: 60, blue: 80, purple: 100, orange: 120, gray: 140, } function smoothTapePath(x: number, y: number, width: number, height: number) { return ` M ${x} ${y} L ${x + width} ${y} L ${x + width} ${y + height} L ${x} ${y + height} Z ` } function tornTapePath(x: number, y: number, width: number, height: number) { return ` M ${x} ${y + 2} L ${x + width * 0.12} ${y} L ${x + width * 0.24} ${y + 3} L ${x + width * 0.36} ${y + 1} L ${x + width * 0.48} ${y + 4} L ${x + width * 0.6} ${y} L ${x + width * 0.72} ${y + 3} L ${x + width * 0.84} ${y + 1} L ${x + width} ${y + 3} L ${x + width - 2} ${y + height} L ${x + width * 0.86} ${y + height - 2} L ${x + width * 0.74} ${y + height + 2} L ${x + width * 0.62} ${y + height - 1} L ${x + width * 0.5} ${y + height + 3} L ${x + width * 0.38} ${y + height} L ${x + width * 0.26} ${y + height + 2} L ${x + width * 0.14} ${y + height - 2} L ${x} ${y + height} Z ` } function sideTornTapePath( x: number, y: number, width: number, height: number ) { return ` M ${x + 4} ${y} L ${x + width - 4} ${y} L ${x + width} ${y + height * 0.12} L ${x + width - 3} ${y + height * 0.24} L ${x + width + 2} ${y + height * 0.36} L ${x + width - 2} ${y + height * 0.48} L ${x + width + 3} ${y + height * 0.6} L ${x + width - 1} ${y + height * 0.72} L ${x + width + 2} ${y + height * 0.84} L ${x + width - 4} ${y + height} L ${x + 4} ${y + height} L ${x} ${y + height * 0.86} L ${x + 3} ${y + height * 0.72} L ${x - 2} ${y + height * 0.58} L ${x + 2} ${y + height * 0.44} L ${x - 3} ${y + height * 0.3} L ${x + 1} ${y + height * 0.16} Z ` } export function Tape({ width = 96, height = 28, variant = "yellow", tapeStyle = "smooth", rotate = -4, opacity = 0.85, roughOptions, className, }: TapeProps) { const svgRef = useRef<SVGSVGElement | null>(null) useEffect(() => { const svg = svgRef.current if (!svg) return svg.replaceChildren() const rc = rough.svg(svg) const padding = 6 const drawWidth = width - padding * 2 const drawHeight = height - padding * 2 const path = tapeStyle === "torn" ? tornTapePath(padding, padding, drawWidth, drawHeight) : tapeStyle === "side-torn" ? sideTornTapePath(padding, padding, drawWidth, drawHeight) : smoothTapePath(padding, padding, drawWidth, drawHeight) const shape = rc.path(path, { seed: roughOptions?.seed ?? seedMap[variant], stroke: roughOptions?.stroke ?? "rgba(17,17,17,0.45)", strokeWidth: roughOptions?.strokeWidth ?? 1.2, fill: roughOptions?.fill ?? colors[variant], fillStyle: roughOptions?.fillStyle ?? "solid", hachureGap: roughOptions?.hachureGap ?? 6, hachureAngle: roughOptions?.hachureAngle ?? -10, roughness: roughOptions?.roughness ?? 1.4, bowing: roughOptions?.bowing ?? 0.8, }) svg.appendChild(shape) }, [width, height, variant, tapeStyle, roughOptions]) return ( <span className={cn("pointer-events-none inline-block", className)} style={{ width, height, transform: `rotate(${rotate}deg)`, opacity, }} aria-hidden="true" > <svg ref={svgRef} width={width} height={height} viewBox={`0 0 ${width} ${height}`} className="h-full w-full" /> </span> ) }
Usage
tsx
<Tape variant="pink" height={45} />
Props
| Prop | Type | Default | Description |
|---|---|---|---|
width | number | 96 | Sets the width of the tape. |
height | number | 28 | Sets the height of the tape. |
variant | TapeVariant | "yellow" | Controls the tape color theme. |
tapeStyle | TapeStyle | "smooth" | Defines the visual style of the tape edges. |
rotate | number | -4 | Rotation angle of the tape in degrees. |
opacity | number | 0.85 | Controls the transparency of the tape. |
className | string | - | Additional classes applied to the tape wrapper. |
roughOptions | RoughTapeOptions | - | 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
<Tape height={45} variant="yellow" tapeStyle="side-torn" roughOptions={{ fillStyle: "cross-hatch", hachureGap: 2, roughness: 2, }} />
Variants
- yellow
- purple
- green
- pink
- blue
- orange
- gray
Tape Styles
- smooth
- torn
- side-torn