Motion in Pindoba is small and shared: one easing curve for state changes, four ready-made animations, one rule for users who ask for less movement, and one focus ring color that flips with the theme mode. There are no duration tokens, so durations are literal values such as 200ms.
Pick a motion, replay it on a Button, Panel or Badge, and watch its easing curve. Use the focus playground at the bottom to Tab through real controls.
animation: spin 1s infinite · Rotates 360deg at a constant speed. Loading indicators.
Press Tab to move through the controls. The ring is the focusRing token, near-white on dark and near-black on light. Focused: nothing yet.
pindobaTransitionThe pdbT utility transitions all properties over the duration you pass, with the system easing cubic-bezier(0.770, 0.000, 0.175, 1.000): a slow start, a fast middle and a soft landing. Use it on interactive elements so every state change feels the same.
import { css } from "@pindoba/styled-system/css";
<button
className={css({
pdbT: "200ms",
backgroundColor: "primary.surface.peak",
_hover: { backgroundColor: "primary.surface.base" },
})}
>
Smooth hover
</button>;
Four animation tokens ship with matching keyframes in the preset. Reference them with the animation property.
| token | value | description |
|---|---|---|
| spin | spin 1s linear infinite | Rotates 360deg. Loading indicators. |
| ping | ping 1s cubic-bezier(0, 0, 0.2, 1) infinite | Scales to 2x and fades out. Attention pulses. |
| pulse | pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite | Dips to the opacity.subtle token. Skeletons. |
| bounce | bounce 1s infinite | Moves up 25% and settles. Scroll hints. |
<span className={css({ animation: "spin" })}>...</span>
Keyframes must live in the preset (keyframes.ts). A @keyframes rule written inline inside a recipe is emitted in the wrong place and does not run.
People who set “reduce motion” in their OS expect no looping or large movement. Panda’s _motionReduce condition targets @media (prefers-reduced-motion: reduce). Switch the animation off there, and shorten transitions to zero:
<span
className={css({
animation: "spin",
_motionReduce: { animation: "none" },
})}
/>
The explorer’s “simulate reduced motion” toggle applies the same rules through an attribute, because a page cannot change the OS media query. Real users get the media query.
Focus is drawn by the focusRing semantic color token: near-white (contrast.lightest) in dark mode and near-black (contrast.darkest) in light mode. Recipes and utilities read the token, so none of them carries a per-mode override.
pindobaOutlinepdbO draws the standard ring: a 2px solid outline, 1px offset, position: relative and z-index: 1, colored with focusRing. Apply it under _focusVisible so it shows for keyboard users only.
<div tabIndex={0} className={css({ _focusVisible: { pdbO: true } })}>
Focusable
</div>
_focusVisibleWithinThe preset adds one custom condition, focusVisibleWithin, which matches an element when any descendant has :focus-visible or carries data-focus-visible. Use it when a wrapper draws the ring for an inner or hidden input, as custom checkboxes and radios do.
import { visuallyHidden } from "@pindoba/styled-system/patterns";
<label
className={css({
_focusVisibleWithin: {
outlineColor: "focusRing",
outlineStyle: "solid",
outlineWidth: "2px",
outlineOffset: "2px",
},
})}
>
<input type="checkbox" className={visuallyHidden()} />
Custom control
</label>;
pdbT for every interactive state change so the easing stays consistentanimation with _motionReduce: { animation: "none" }focusRing (or pdbO), never a hard-coded color_focusVisible, not _focus, so mouse clicks do not leave a ring