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 · lighticonslucide-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;
}
}