Browse the docs
MotionNo. 107 of 145
Stamp
A rubber stamp that slams onto the page: it starts large and transparent and lands rotated with a small overshoot.
Installation
pnpm dlx shadcn@latest add @corsair-ui/stampNo setup needed: the CLI finds @corsair-ui in the shadcn registry directory. The files land in your project as source: edit them like your own code. More in Installation.
- Also adds
- utilsuse-in-viewuse-entrance
- npm
clsxtailwind-merge
Examples
Sold out
trigger="load" on a card; the button stamps it again.
Thursday, 20:00
Berth 2
14 of 14 aboard
Sold outAngles and weight
rotate, from and duration. They land when they scroll into view.
PaidApprovedDraft
API
| Prop | Type | Default |
|---|---|---|
rotate / fromWhere it lands in degrees, and the scale it starts from. | number | -8 / 1.6 |
trigger / playAs in blur-text. | "load" | "in-view" / boolean | "in-view" |
delay / durationIn ms. | number | 0 / 320 |
Styling
Restyle it in your project: className on each part, the theme variables, and these attributes with arbitrary variants, e.g. className="[&_[data-slot=stamp]]:text-primary".
- It is an inline-block with nothing but the rotation: give it the stamp look with classes (border-2, uppercase, tracking-widest, a colour).
- data-slot
stamp
- State
data-statecomputed
Accessibility
- The content stays readable to screen readers. With prefers-reduced-motion it holds still and shows the finished state.
Compatibility
- Tailwind CSS
- 3.4 and 4, both checked in CI
- React
- 19
- Rendering
- Client component ("use client")
- Motion
- Respects prefers-reduced-motion
Source
1 file, installed as source you own
stamp.tsxView on GitHub"use client";
import { useMemo, type ComponentProps, type CSSProperties, type Ref } from "react";
import { useEntrance, type EntranceTrigger } from "@/registry/default/hooks/use-entrance";
import { cn } from "@/registry/default/lib/utils";
function mergeRefs<T>(...refs: (Ref<T> | undefined)[]) {
return (node: T | null) => {
for (const ref of refs) {
if (typeof ref === "function") ref(node);
else if (ref) ref.current = node;
}
};
}
interface StampProps extends Omit<ComponentProps<"span">, "onAnimationEnd"> {
as?: "span" | "div";
/** Angle the stamp lands at, in degrees. */
rotate?: number;
/** Scale the stamp starts from before it slams down. */
from?: number;
/** "load" plays on first paint with CSS alone; "in-view" when the stamp scrolls into view. */
trigger?: EntranceTrigger;
/** In-view: play the first time only. */
once?: boolean;
/** Takes over from `trigger`: `false` holds the stamp up, `true` slams it down. */
play?: boolean;
/** Wait before the stamp falls, in ms. */
delay?: number;
/** How long the slam takes, in ms. */
duration?: number;
/** Called once the stamp has landed. */
onAnimationComplete?: () => void;
}
/**
* A rubber stamp that slams onto the page: it starts large and transparent,
* then lands rotated with a small overshoot. The content stays readable for
* screen readers, and the server HTML and `prefers-reduced-motion` show the
* stamp already in place.
*
* @example
* <Stamp className="border-primary text-primary rounded-md border-2 px-3 py-1 font-bold tracking-widest uppercase">Sold out</Stamp>
*/
function Stamp({
as: Tag = "span",
rotate = -8,
from = 1.6,
trigger = "in-view",
once = true,
play,
delay = 0,
duration = 320,
onAnimationComplete,
className,
style,
ref,
...props
}: StampProps) {
const [observe, phase] = useEntrance<HTMLElement>({ trigger, once, play });
const mergedRef = useMemo(() => mergeRefs(ref, observe), [ref, observe]);
const stampStyle = {
"--stamp-rotate": `${rotate}deg`,
"--stamp-from": `${from}`,
...(phase === "static"
? {}
: {
animationDuration: `${duration}ms`,
animationDelay: `${delay}ms`,
animationPlayState: phase === "armed" ? "paused" : undefined,
}),
...style,
} as CSSProperties;
return (
<Tag
ref={mergedRef as Ref<never>}
data-slot="stamp"
data-state={phase}
className={cn(
"inline-block [transform:rotate(var(--stamp-rotate))]",
phase !== "static" && "motion-safe:animate-stamp",
className
)}
style={stampStyle}
onAnimationEnd={(event) => {
if (event.target === event.currentTarget) onAnimationComplete?.();
}}
{...props}
/>
);
}
export { Stamp, type StampProps };