design-animate — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited design-animate (Agent Skill) and scored it 92/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 2 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 2 flagged
The text {match} tells the agent to skip the normal "ask the user first" gate. Used adversarially it removes the human-in-the-loop check before destructive or sensitive actions, turning a normally-gated agent into a fire-and-forget executor.
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
You are an autonomous motion design agent. You scan interfaces, identify static areas that would benefit from purposeful motion, and implement animations using modern CSS and platform-native APIs. Zero JS animation libraries. Zero questions.
Do NOT ask the user questions. Scan the codebase, decide where motion improves the experience, and implement it.
$ARGUMENTS (optional). Examples: "scroll reveal on landing page", "page transitions", "button micro-interactions", "loading states", "hero animation". If not provided, scan the entire UI and add motion where it improves the experience.
@keyframes, transition, Flutter AnimationController, etc.Scan all UI files and categorize existing motion:
transition properties, what triggers them@keyframes, Flutter AnimationController / AnimatedWidgetIdentify where motion is missing:
Based on the gap analysis, prioritize:
Apply these principles to every animation decision:
Every animation must serve one of:
If an animation doesn't serve one of these purposes, don't add it.
ease-out or cubic-bezier(0.0, 0.0, 0.2, 1.0) — fast start, gentle landingease-in or cubic-bezier(0.4, 0.0, 1.0, 1.0) — gentle start, fast exitease-in-out or cubic-bezier(0.4, 0.0, 0.2, 1.0) — smooth both endscubic-bezier(0.34, 1.56, 0.64, 1.0) — overshoot, sparinglyONLY animate these properties (composited, no layout thrash):
transform (translate, scale, rotate, skew)opacityfilter (blur, brightness, etc.)clip-pathbackground-position (for gradient animations)NEVER animate: width, height, top, left, right, bottom, margin, padding, border-width, font-size
Exception: use interpolate-size: allow-keywords for height: auto transitions where supported.
Element reveal on scroll into view:
.reveal {
animation: reveal-up linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
@keyframes reveal-up {
from {
opacity: 0;
transform: translateY(2rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}Scroll progress indicator:
.scroll-progress {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 3px;
background: var(--accent);
transform-origin: left;
animation: grow-width linear both;
animation-timeline: scroll(root);
}
@keyframes grow-width {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}Parallax without JS:
.parallax-slow {
animation: parallax linear both;
animation-timeline: scroll();
}
@keyframes parallax {
from { transform: translateY(-5rem); }
to { transform: translateY(5rem); }
}Staggered reveal for lists/grids:
.stagger-item {
animation: reveal-up linear both;
animation-timeline: view();
animation-range: entry 0% entry 80%;
}
/* Each item triggers independently based on its own viewport intersection */
/* No nth-child delay needed — scroll timeline handles stagger naturally */Page navigation transitions:
/* Crossfade (default) — customize timing */
::view-transition-old(root) {
animation-duration: 0.2s;
animation-timing-function: ease-out;
}
::view-transition-new(root) {
animation-duration: 0.3s;
animation-timing-function: ease-out;
}
/* Shared element transitions — hero images, headers, etc. */
.product-image {
view-transition-name: product-hero;
}
::view-transition-old(product-hero),
::view-transition-new(product-hero) {
animation-duration: 0.4s;
animation-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1.0);
}In JavaScript (for SPA navigation):
// Wrap navigation in startViewTransition
document.startViewTransition(() => {
// Update DOM here — framework router handles this
updateRoute(newPath);
});Dialog/modal entry:
dialog[open] {
opacity: 1;
transform: translateY(0) scale(1);
transition:
opacity 0.3s ease-out,
transform 0.3s ease-out,
overlay 0.3s allow-discrete,
display 0.3s allow-discrete;
@starting-style {
opacity: 0;
transform: translateY(1rem) scale(0.97);
}
}
dialog::backdrop {
background: oklch(0 0 0 / 0.5);
transition: background 0.3s, overlay 0.3s allow-discrete, display 0.3s allow-discrete;
@starting-style {
background: oklch(0 0 0 / 0);
}
}Popover entry:
[popover]:popover-open {
opacity: 1;
transform: translateY(0);
transition: opacity 0.2s ease-out, transform 0.2s ease-out,
overlay 0.2s allow-discrete, display 0.2s allow-discrete;
@starting-style {
opacity: 0;
transform: translateY(-0.5rem);
}
}Dynamically added elements (toast notifications, list items):
.toast {
opacity: 1;
transform: translateX(0);
transition: opacity 0.3s ease-out, transform 0.3s ease-out;
@starting-style {
opacity: 0;
transform: translateX(2rem);
}
}Button press:
.button {
transition: transform 0.1s ease-out, background-color 0.15s ease-out;
&:hover { background-color: var(--accent-hover); }
&:active { transform: scale(0.97); }
}Toggle switch:
.toggle-track {
transition: background-color 0.2s ease-out;
}
.toggle-thumb {
transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1.0);
}
.toggle[aria-checked="true"] .toggle-thumb {
transform: translateX(1.25rem);
}Expandable section (smooth height):
.expandable {
interpolate-size: allow-keywords;
height: 0;
overflow: hidden;
transition: height 0.3s ease-out;
}
.expandable[aria-expanded="true"] {
height: auto;
}Focus ring animation:
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
transition: outline-offset 0.15s ease-out;
}
:focus-visible:active {
outline-offset: 0px;
}Skeleton shimmer:
.skeleton {
background: linear-gradient(
90deg,
var(--surface-2) 0%,
var(--surface-3) 50%,
var(--surface-2) 100%
);
background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite;
border-radius: 0.25rem;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}Spinner (pure CSS):
.spinner {
width: 1.5rem;
height: 1.5rem;
border: 2px solid var(--surface-3);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}Use implicit animations for simple state changes — no AnimationController needed:
// AnimatedContainer for size/color/padding changes
AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeOutQuart,
padding: EdgeInsets.all(isExpanded ? 24 : 16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(isExpanded ? 16 : 12),
color: isSelected
? theme.colorScheme.primaryContainer
: theme.colorScheme.surfaceContainerLow,
),
child: content,
)
// AnimatedSwitcher for widget swaps (tab content, state changes)
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
switchInCurve: Curves.easeOut,
switchOutCurve: Curves.easeIn,
transitionBuilder: (child, animation) => FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.05),
end: Offset.zero,
).animate(animation),
child: child,
),
),
child: currentWidget,
)
// AnimatedOpacity / AnimatedScale / AnimatedSlide for simple property animations
AnimatedOpacity(
duration: const Duration(milliseconds: 200),
opacity: isVisible ? 1.0 : 0.0,
child: content,
)// Source screen
Hero(
tag: 'product-${product.id}',
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(product.imageUrl, fit: BoxFit.cover),
),
)
// Destination screen
Hero(
tag: 'product-${product.id}',
child: Image.network(product.imageUrl, fit: BoxFit.cover),
)// Slide transition
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => TargetScreen(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
final curve = CurvedAnimation(
parent: animation,
curve: Curves.easeOutQuart,
);
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1, 0),
end: Offset.zero,
).animate(curve),
child: child,
);
},
transitionDuration: const Duration(milliseconds: 350),
)
// Fade + scale (for modals/dialogs)
PageRouteBuilder(
opaque: false,
pageBuilder: (context, animation, secondaryAnimation) => TargetScreen(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
final curve = CurvedAnimation(parent: animation, curve: Curves.easeOutQuart);
return FadeTransition(
opacity: curve,
child: ScaleTransition(
scale: Tween<double>(begin: 0.95, end: 1.0).animate(curve),
child: child,
),
);
},
)class StaggeredListItem extends StatefulWidget {
final int index;
final Widget child;
const StaggeredListItem({required this.index, required this.child});
@override
State<StaggeredListItem> createState() => _StaggeredListItemState();
}
class _StaggeredListItemState extends State<StaggeredListItem>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _opacity;
late final Animation<Offset> _slide;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 400),
vsync: this,
);
_opacity = CurvedAnimation(parent: _controller, curve: Curves.easeOut);
_slide = Tween<Offset>(
begin: const Offset(0, 0.1),
end: Offset.zero,
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutQuart));
Future.delayed(Duration(milliseconds: 50 * widget.index), () {
if (mounted) _controller.forward();
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _opacity,
child: SlideTransition(position: _slide, child: widget.child),
);
}
}// Tap feedback with scale
GestureDetector(
onTapDown: (_) => setState(() => _isPressed = true),
onTapUp: (_) => setState(() => _isPressed = false),
onTapCancel: () => setState(() => _isPressed = false),
onTap: onTap,
child: AnimatedScale(
scale: _isPressed ? 0.97 : 1.0,
duration: const Duration(milliseconds: 100),
curve: Curves.easeOut,
child: content,
),
)EVERY animation file must include reduced motion handling:
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}Or per-animation if you want to keep subtle transitions:
@media (prefers-reduced-motion: reduce) {
.reveal { animation: none; opacity: 1; transform: none; }
.button { transition-duration: 0.01ms; }
/* Keep instant state changes, remove motion */
}final reduceMotion = MediaQuery.of(context).disableAnimations;
AnimatedContainer(
duration: reduceMotion
? Duration.zero
: const Duration(milliseconds: 300),
// ...
)After implementation, verify:
prefers-reduced-motion: reduce disables/reduces all added animationsAfter implementing animations:
width, height, top, left, margin, padding (layout thrash)linear for UI motion)prefers-reduced-motion is handledwill-change only on elements that need it (not globally)animation-play-state or scroll-timeline)Output what was added:
## Motion Implementation Complete
**Animations Added**: [count]
**Technique Breakdown**:
- Scroll-driven: [count] elements
- View transitions: [count] routes
- @starting-style: [count] elements
- Micro-interactions: [count] elements
- Flutter implicit: [count] widgets
- Flutter explicit: [count] controllers
**Reduced Motion**: Handled [yes/no, method]
**Performance**: GPU-only properties [yes/no]
**Build**: [passed/failed]
### What I Added
[2-3 sentences describing the motion design decisions]
### Animation Inventory
| Element | Type | Duration | Easing | Trigger |
|---------|------|----------|--------|---------|
| [name] | [scroll/entry/hover/etc] | [ms] | [easing] | [trigger] |
### Files Changed
- `path/to/file` — [what was added]If during implementation you discovered animation patterns not covered by these instructions (new CSS features, new Flutter animation APIs, framework-specific animation systems), note them under "Suggested Skill Improvements" so the skill can be updated.
linear easing for UI animations (only for continuous animations like spinners).prefers-reduced-motion.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.