ericts
uicheck-marksource: ok · hit
Check Mark
A pure SVG checkmark animation that draws a square or circle before the check.
npx shadcn add "https://ui.ericts.com/r/check-mark.json"free
No selection tags yet — empty means no evidence, not lack of support.
Files2 · source cached 10/14/2026, 1:21:48 PM
registry/base/ui/check-mark.tsx
import * as React from "react";
import { cn } from "@/lib/utils";
import "./check-mark.css";
export type CheckMarkVariant = "square" | "circle";
export type CheckMarkSize = "sm" | "md" | "lg";
export interface CheckMarkProps
extends Omit<React.ComponentProps<"span">, "children"> {
variant?: CheckMarkVariant;
size?: CheckMarkSize;
/** Accessible label. Omit for a decorative mark. */
label?: string;
strokeWidth?: number;
}
const sizeClassNames: Record<CheckMarkSize, string> = {
sm: "size-5",
md: "size-6",
lg: "size-10",
};
const DEFAULT_STROKE_WIDTH = 2;
const VECTOR_EFFECT = "non-scaling-stroke";
const SQUARE_PATH = "M 50,11.5 L 88.5,11.5 L 88.5,88.5 L 11.5,88.5 L 11.5,11.5 Z";
const CIRCLE_PATH =
"M 88.5,50 C 88.5,71.26 71.26,88.5 50,88.5 C 28.74,88.5 11.5,71.26 11.5,50 C 11.5,28.74 28.74,11.5 50,11.5 C 71.26,11.5 88.5,28.74 88.5,50 Z";
const CHECK_PATH = "M 32.3,51 L 44,62.9 L 68.75,36.5";
const shapePaths: Record<CheckMarkVariant, string> = {
square: SQUARE_PATH,
circle: CIRCLE_PATH,
};
export function CheckMark({
variant = "square",
size,
label,
strokeWidth = DEFAULT_STROKE_WIDTH,
"aria-label": ariaLabel,
"aria-hidden": ariaHidden,
className,
...props
}: CheckMarkProps) {
const accessibleLabel = label ?? ariaLabel;
return (
<span
data-slot="check-mark"
data-variant={variant}
role={accessibleLabel ? "img" : undefined}
aria-label={accessibleLabel}
aria-hidden={accessibleLabel ? undefined : (ariaHidden ?? true)}
className={cn(
"check-mark inline-flex shrink-0 items-center justify-center text-foreground",
size ? sizeClassNames[size] : "w-full max-w-80",
className,
)}
{...props}
>
<svg
viewBox="0 0 100 100"
aria-hidden="true"
className={size ? "size-full" : "h-auto w-full"}
>
<path
className="check-mark-shape"
d={shapePaths[variant]}
fill="none"
stroke="currentColor"
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeLinejoin="round"
vectorEffect={VECTOR_EFFECT}
pathLength="100"
strokeDasharray="100 100"
strokeDashoffset="100"
/>
<path
className="check-mark-checkmark"
d={CHECK_PATH}
fill="none"
stroke="currentColor"
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeLinejoin="round"
vectorEffect={VECTOR_EFFECT}
pathLength="100"
strokeDasharray="100 100"
strokeDashoffset="100"
/>
</svg>
</span>
);
}
registry/base/ui/check-mark.css
/* Defaults are layered for customization. Normal unlayered styles and
utilities in a later layer can override them. */
@layer components {
.check-mark {
--check-mark-ease: cubic-bezier(0.645, 0.045, 0.355, 1);
--check-mark-shape-duration: 600ms;
--check-mark-check-duration: 400ms;
}
.check-mark-shape {
animation: check-mark-draw var(--check-mark-shape-duration)
var(--check-mark-ease) forwards;
transform: rotate(-90deg);
transform-origin: 50% 50%;
}
.check-mark-checkmark {
animation: check-mark-draw var(--check-mark-check-duration)
var(--check-mark-ease) var(--check-mark-shape-duration) forwards;
}
@keyframes check-mark-draw {
to {
stroke-dashoffset: 0;
}
}
}
/* Motion guards override ordinary customization, including inline timing.
Keep these outside the defaults layer; only motion declarations are important. */
@media (prefers-reduced-motion: reduce) {
.check-mark-shape,
.check-mark-checkmark {
animation-duration: 1ms !important;
animation-delay: 0ms !important;
}
}