shadimage
ericts
uijittersource: ok · hit

Jitter

A jitter animation primitive plus a ready-made vibration icon.

npx shadcn add "https://ui.ericts.com/r/jitter.json"
free

No selection tags yet — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
iconslucide-react

Files2 · source cached 10/14/2026, 1:24:26 PM

registry/base/ui/jitter.tsx
import * as React from "react";
import { AlarmClock } from "lucide-react";

import { cn } from "@/lib/utils";

import "./jitter.css";

export type JitterAxis = "both" | "horizontal" | "vertical";
export type JitterIconAxis = JitterAxis;

export type JitterProps = React.ComponentProps<"span"> & {
  /** The axis the content jitters on. Defaults to horizontal. */
  axis?: JitterAxis;
  /** Classes applied to the animated child wrapper. */
  targetClassName?: string;
};

export function Jitter({
  axis = "horizontal",
  className,
  children,
  targetClassName,
  ...props
}: JitterProps) {
  return (
    <span
      data-slot="jitter"
      data-axis={axis}
      className={cn("jitter inline-flex", className)}
      {...props}
    >
      <span
        data-slot="jitter-target"
        className={cn("jitter-target inline-flex", targetClassName)}
      >
        {children}
      </span>
    </span>
  );
}

export type JitterIconProps = React.ComponentProps<"div"> & {
  /** The axis the icon jitters on. Defaults to horizontal. */
  axis?: JitterAxis;
};

export function JitterIcon({
  axis = "horizontal",
  className,
  ...props
}: JitterIconProps) {
  return (
    <div
      data-slot="jitter-icon"
      data-axis={axis}
      className={cn(
        "jitter jitter-icon flex flex-col items-center text-foreground",
        className,
      )}
      {...props}
    >
      <div
        data-slot="jitter-icon-target"
        className="jitter-target w-full max-w-72"
      >
        <AlarmClock aria-hidden="true" className="h-auto w-full" />
      </div>
    </div>
  );
}
registry/base/ui/jitter.css
/* Defaults are layered for customization. Normal unlayered styles and
   utilities in a later layer can override them. */
@layer components {
  .jitter,
  .jitter-icon {
    --jitter-x: 3px;
    --jitter-y: 0px;
  }

  .jitter-target {
    backface-visibility: hidden;
    transform-origin: center;
    animation: jitter 480ms steps(1, end) both;
  }

  .jitter[data-axis="vertical"],
  .jitter-icon[data-axis="vertical"] {
    --jitter-x: 0px;
    --jitter-y: 3px;
  }

  .jitter[data-axis="both"],
  .jitter-icon[data-axis="both"] {
    --jitter-x: 3px;
    --jitter-y: 2px;
  }

  .jitter[data-axis="both"] .jitter-target {
    animation-name: jitter-both;
  }

  @keyframes jitter {
    0%,
    100% {
      transform: translate3d(0, 0, 0);
    }
    8% {
      transform: translate3d(
        calc(var(--jitter-x) * -1),
        calc(var(--jitter-y) * 1),
        0
      );
    }
    16% {
      transform: translate3d(
        calc(var(--jitter-x) * 0.9),
        calc(var(--jitter-y) * -0.9),
        0
      );
    }
    26% {
      transform: translate3d(
        calc(var(--jitter-x) * -0.8),
        calc(var(--jitter-y) * 0.8),
        0
      );
    }
    36% {
      transform: translate3d(
        calc(var(--jitter-x) * 0.67),
        calc(var(--jitter-y) * -0.67),
        0
      );
    }
    48% {
      transform: translate3d(
        calc(var(--jitter-x) * -0.52),
        calc(var(--jitter-y) * 0.52),
        0
      );
    }
    60% {
      transform: translate3d(
        calc(var(--jitter-x) * 0.4),
        calc(var(--jitter-y) * -0.4),
        0
      );
    }
    72% {
      transform: translate3d(
        calc(var(--jitter-x) * -0.27),
        calc(var(--jitter-y) * 0.27),
        0
      );
    }
    84% {
      transform: translate3d(
        calc(var(--jitter-x) * 0.15),
        calc(var(--jitter-y) * -0.15),
        0
      );
    }
    92% {
      transform: translate3d(
        calc(var(--jitter-x) * -0.07),
        calc(var(--jitter-y) * 0.07),
        0
      );
    }
  }

  /* Multipliers reproduce the tuned 3px/2px path at the default custom
     property values, while still following consumer --jitter-x/--jitter-y. */
  @keyframes jitter-both {
    0%,
    100% {
      transform: translate3d(0, 0, 0);
    }
    8% {
      transform: translate3d(
        calc(var(--jitter-x) * -1),
        calc(var(--jitter-y) * 0.9),
        0
      );
    }
    16% {
      transform: translate3d(
        calc(var(--jitter-x) * 0.9),
        calc(var(--jitter-y) * -0.9),
        0
      );
    }
    26% {
      transform: translate3d(
        calc(var(--jitter-x) * -0.8),
        calc(var(--jitter-y) * -0.8),
        0
      );
    }
    36% {
      transform: translate3d(
        calc(var(--jitter-x) * 2 / 3),
        calc(var(--jitter-y) * 0.67),
        0
      );
    }
    48% {
      transform: translate3d(
        calc(var(--jitter-x) * -0.52),
        calc(var(--jitter-y) * 0.52),
        0
      );
    }
    60% {
      transform: translate3d(
        calc(var(--jitter-x) * 0.4),
        calc(var(--jitter-y) * -0.4),
        0
      );
    }
    72% {
      transform: translate3d(
        calc(var(--jitter-x) * -0.27),
        calc(var(--jitter-y) * -0.27),
        0
      );
    }
    84% {
      transform: translate3d(
        calc(var(--jitter-x) * 0.15),
        calc(var(--jitter-y) * 0.15),
        0
      );
    }
    92% {
      transform: translate3d(
        calc(var(--jitter-x) * -0.07),
        calc(var(--jitter-y) * 0.07),
        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) {
  .jitter-target {
    animation: none !important;
  }
}

More from ericts

Browse all components in this library →