Signature
An animated SVG signature effect that draws out text as if hand-written.
Live preview recorded from Dqnamo Kitchen; component and provider source published by JP / dqnamo. Preview: platform recorded · rights authorized.
A reusable postage stamp frame for images, text, and custom React content.
dSource LanddqnamoWhy it stands out
A reusable postage-stamp frame wraps images, text, or arbitrary React content in a perforated paper edge. The example presents the frame as a small collectible object with layered content and a restrained hover response.
import type { CSSProperties, ReactNode } from "react";
import { cn } from "@/helpers/classname-helper";
const stampShadow =
"drop-shadow(0 1px 1px rgb(15 15 15 / 12%)) drop-shadow(0 12px 22px rgb(15 15 15 / 9%))";
const stampBackground =
"linear-gradient(145deg, rgb(255 255 255 / 34%), transparent 42%), var(--stamp-paper)";
type StampCustomProperty = "--stamp-ink" | "--stamp-padding" | "--stamp-paper";
type StampRootStyle = CSSProperties &
Partial<Record<StampCustomProperty, string>>;
type CssLength = number | string;
export type StampProps = {
/** Any React content rendered inside the perforated paper frame. */
children: ReactNode;
"aria-hidden"?: boolean;
"aria-label"?: string;
className?: string;
contentClassName?: string;
horizontalPerforations?: number;
ink?: string;
padding?: CssLength;
paper?: string;
perforationDepth?: number;
stampClassName?: string;
style?: CSSProperties;
verticalPerforations?: number;
};
function toCssLength(value: CssLength) {
return typeof value === "number" ? `${value}px` : value;
}
function clamp(value: number, minimum: number, maximum: number) {
return Math.min(Math.max(value, minimum), maximum);
}
function createStampClipPath({
depth,
horizontalCount,
verticalCount,
}: {
depth: number;
horizontalCount: number;
verticalCount: number;
}) {
const resolvedDepth = clamp(depth, 0, 10);
const resolvedHorizontalCount = clamp(Math.round(horizontalCount), 4, 32);
const resolvedVerticalCount = clamp(Math.round(verticalCount), 4, 40);
const horizontalStep = 100 / resolvedHorizontalCount;
const verticalStep = 100 / resolvedVerticalCount;
const points: Array<[number, number]> = [[0, 0]];
for (let index = 0; index < resolvedHorizontalCount; index += 1) {
const start = index * horizontalStep;
points.push([start + horizontalStep * 0.25, 0]);
points.push([start + horizontalStep * 0.5, resolvedDepth]);
points.push([start + horizontalStep * 0.75, 0]);
points.push([start + horizontalStep, 0]);
}
for (let index = 0; index < resolvedVerticalCount; index += 1) {
const start = index * verticalStep;
points.push([100, start + verticalStep * 0.25]);
points.push([100 - resolvedDepth, start + verticalStep * 0.5]);
points.push([100, start + verticalStep * 0.75]);
points.push([100, start + verticalStep]);
}
for (let index = 0; index < resolvedHorizontalCount; index += 1) {
const start = 100 - index * horizontalStep;
points.push([start - horizontalStep * 0.25, 100]);
points.push([start - horizontalStep * 0.5, 100 - resolvedDepth]);
points.push([start - horizontalStep * 0.75, 100]);
points.push([start - horizontalStep, 100]);
}
for (let index = 0; index < resolvedVerticalCount; index += 1) {
const start = 100 - index * verticalStep;
points.push([0, start - verticalStep * 0.25]);
points.push([resolvedDepth, start - verticalStep * 0.5]);
points.push([0, start - verticalStep * 0.75]);
points.push([0, start - verticalStep]);
}
return `polygon(${points
.map(([x, y]) => `${x.toFixed(3)}% ${y.toFixed(3)}%`)
.join(", ")})`;
}
export function Stamp({
"aria-hidden": ariaHidden,
"aria-label": ariaLabel,
children,
className,
contentClassName,
horizontalPerforations = 12,
ink = "#181713",
padding = 13,
paper = "#ffffff",
perforationDepth = 2.4,
stampClassName,
style,
verticalPerforations = 15,
}: StampProps) {
const clipPath = createStampClipPath({
depth: perforationDepth,
horizontalCount: horizontalPerforations,
verticalCount: verticalPerforations,
});
const rootStyle: StampRootStyle = {
"--stamp-ink": ink,
"--stamp-padding": toCssLength(padding),
"--stamp-paper": paper,
filter: stampShadow,
...style,
};
return (
<div
className={cn("relative aspect-[4/5] w-[min(100%,14rem)]", className)}
style={rootStyle}
>
<article
aria-hidden={ariaHidden}
aria-label={ariaHidden ? undefined : ariaLabel}
className={cn(
"relative size-full overflow-hidden p-[var(--stamp-padding)] text-[var(--stamp-ink)]",
stampClassName,
)}
style={{ background: stampBackground, clipPath }}
>
<div
className={cn(
"relative size-full min-h-0 min-w-0 overflow-hidden",
contentClassName,
)}
>
{children}
</div>
</article>
</div>
);
}Discovery vocabulary
Related by governed terms
An animated SVG signature effect that draws out text as if hand-written.
More from dqnamo