shadimage
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;
  }
}

More from ericts

Browse all components in this library →