ui-micro

افزودن بازخوردهای ظریف و کاربردی به دکمه‌ها، فرم‌ها و کنترل‌های رابط کاربری

این skill برای طراحی و پیاده‌سازی micro-interactionهای هدفمند در رابط کاربری است؛ از حالت فشرده‌شدن دکمه و ripple گرفته تا انیمیشن toggle، checkbox، tooltip، خطای فرم و وضعیت موفقیت.

با تکیه بر اصول Material Design 3، Apple HIG و توکن‌های استاندارد motion، زمان‌بندی و easing مناسب پیشنهاد می‌دهد و برای CSS، Framer Motion و Web Animations API الگوهای آماده ارائه می‌کند. پشتیبانی از prefers-reduced-motion نیز تجربه‌ای دسترس‌پذیرتر می‌سازد.

چه چیزی می‌گیرید

  • الگوهای انیمیشن برای دکمه، toggle، checkbox، radio و segmented control
  • بازخوردهای واضح برای focus، loading، success، error و validation فرم
  • انیمیشن‌های tooltip، hover lift، ripple و label شناور
  • زمان‌بندی، easing و spring مناسب بر اساس نوع تعامل
  • جایگزین‌های کم‌تحرک برای کاربران دارای `prefers-reduced-motion`

برای چه کاری خوب است

  • محصولات وب و اپلیکیشن‌هایی که به polish بیشتر برای تعاملات روزمره نیاز دارند
  • طراحان و توسعه‌دهندگانی که می‌خواهند states و feedback کامپوننت‌ها را استاندارد کنند
  • فرم‌ها، داشبوردها و سیستم‌های طراحی دارای دکمه‌ها و کنترل‌های تعاملی متعدد

برای چه کاری نیست

  • ساخت انیمیشن‌های بزرگ صفحه، motion graphics یا ویدیو
  • پروژه‌هایی که فقط به تغییرات ایستا در ظاهر کامپوننت‌ها نیاز دارند
  • تعاملاتی که انیمیشن در آن‌ها می‌تواند سرعت یا دسترس‌پذیری را مختل کند

دربارهٔ این اسکیل

UI Micro-Interactions

Small, purposeful animations that enhance user experience.

Standards Reference

StandardDocumentation
W3C DTCGdesigntokens.org
Material Design 3m3.material.io/foundations/interaction
Apple HIGdeveloper.apple.com/design/human-interface-guidelines

Quick Reference

Uses tokens from: tokens/motion.tokens.json, tokens/spring.tokens.json

Interaction Timings

InteractionDurationEasing
Button press100msease-out
Toggle switch200msspring
Tooltip show150msease-out
Form error300msbounce
Checkbox200msease-out

Selection Guide

What micro-interaction?
│
├── BUTTON STATES → Press feedback
│   ├── Scale down → tactile feel
│   ├── Ripple → material style
│   ├── Glow → highlight state
│   └── Color shift → state indication
│
├── FORM ELEMENTS → Input feedback
│   ├── Focus → highlight border
│   ├── Error → shake + red
│   ├── Success → checkmark + green
│   └── Loading → spinner in field
│
├── TOGGLES → State switches
│   ├── Switch → slide animation
│   ├── Checkbox → check draw
│   ├── Radio → fill animation
│   └── Segmented → indicator slide
│
├── FEEDBACK → User acknowledgment
│   ├── Success → checkmark pop
│   ├── Error → shake
│   ├── Warning → pulse
│   └── Info → subtle bounce
│
└── HOVER EFFECTS → Cursor feedback
    ├── Scale → grow on hover
    ├── Lift → shadow + translate
    ├── Glow → subtle highlight
    └── Underline → link indication

Patterns

Button Press

Tactile feedback on click.

CSS:

.button {
  transition: transform 100ms ease-out, box-shadow 100ms ease-out;
  /* {motion.duration.short-2} */
}

.button:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

.button:active {
  transform: translateY(0) scale(0.98);
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

Framer Motion:

<motion.button
  whileHover={{ scale: 1.02, y: -2 }}
  whileTap={{ scale: 0.98 }}
  transition={{ type: 'spring', stiffness: 400, damping: 17 }}
>
  Click me
</motion.button>

Material Ripple

Expanding ripple from click point.

JavaScript:

function createRipple(event) {
  const button = event.currentTarget;
  const ripple = document.createElement('span');

  const rect = button.getBoundingClientRect();
  const size = Math.max(rect.width, rect.height);
  const x = event.clientX - rect.left - size / 2;
  const y = event.clientY - rect.top - size / 2;

  ripple.style.cssText = `
    position: absolute;
    width: ${size}px;
    height: ${size}px;
    left: ${x}px;
    top: ${y}px;
    background: rgba(255, 255, 255, 0.3);
    border-radius: 50%;
    transform: scale(0);
    pointer-events: none;
  `;

  button.style.position = 'relative';
  button.style.overflow = 'hidden';
  button.appendChild(ripple);

  ripple.animate([
    { transform: 'scale(0)', opacity: 1 },
    { transform: 'scale(2)', opacity: 0 }
  ], {
    duration: 600,
    easing: 'cubic-bezier(0.4, 0, 0.2, 1)'
  }).onfinish = () => ripple.remove();
}

Toggle Switch

Smooth sliding toggle.

CSS:

.toggle {
  width: 52px;
  height: 32px;
  background: #ccc;
  border-radius: 16px;
  padding: 2px;
  cursor: pointer;
  transition: background 200ms ease-out;
}

.toggle.active {
  background: #3b82f6;
}

.toggle-handle {
  width: 28px;
  height: 28px;
  background: white;
  border-radius: 50%;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
  transition: transform 200ms cubic-bezier(0.34, 1.56, 0.64, 1);
  /* Spring-like easing */
}

.toggle.active .toggle-handle {
  transform: translateX(20px);
}

Framer Motion:

const Toggle = ({ isOn, onToggle }) => (
  <motion.div
    className="toggle"
    onClick={onToggle}
    animate={{ backgroundColor: isOn ? '#3b82f6' : '#ccc' }}
  >
    <motion.div
      className="toggle-handle"
      animate={{ x: isOn ? 20 : 0 }}
      transition={{ type: 'spring', stiffness: 500, damping: 30 }}
    />
  </motion.div>
);

Checkbox Animation

Checkmark draw-on effect.

CSS:

.checkbox {
  width: 24px;
  height: 24px;
  border: 2px solid #ccc;
  border-radius: 4px;
  position: relative;
  transition: border-color 150ms, background 150ms;
}

.checkbox.checked {
  border-color: #3b82f6;
  background: #3b82f6;
}

.checkbox .checkmark {
  position: absolute;
  top: 4px;
  left: 7px;
  width: 6px;
  height: 12px;
  border: solid white;
  border-width: 0 2px 2px 0;
  transform: rotate(45deg) scale(0);
  transform-origin: bottom right;
  transition: transform 200ms cubic-bezier(0.34, 1.56, 0.64, 1);
}

.checkbox.checked .checkmark {
  transform: rotate(45deg) scale(1);
}

SVG Draw-on:

function animateCheckmark(svg) {
  const path = svg.querySelector('path');
  const length = path.getTotalLength();

  path.style.strokeDasharray = length;
  path.style.strokeDashoffset = length;

  path.animate([
    { strokeDashoffset: length },
    { strokeDashoffset: 0 }
  ], {
    duration: 300,
    easing: 'ease-out',
    fill: 'forwards'
  });
}

Input Focus

Enhanced focus state.

CSS:

.input {
  border: 2px solid #e5e7eb;
  border-radius: 8px;
  padding: 12px 16px;
  transition: border-color 150ms, box-shadow 150ms;
}

.input:focus {
  outline: none;
  border-color: #3b82f6;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
}

/* Label float animation */
.input-wrapper {
  position: relative;
}

.input-label {
  position: absolute;
  left: 16px;
  top: 50%;
  transform: translateY(-50%);
  color: #6b7280;
  transition: all 150ms ease-out;
  pointer-events: none;
}

.input:focus + .input-label,
.input:not(:placeholder-shown) + .input-label {
  top: 0;
  transform: translateY(-50%) scale(0.85);
  background: white;
  padding: 0 4px;
  color: #3b82f6;
}

Error Shake

Form validation feedback.

CSS:

@keyframes shake {
  0%, 100% { transform: translateX(0); }
  10%, 30%, 50%, 70%, 90% { transform: translateX(-4px); }
  20%, 40%, 60%, 80% { transform: translateX(4px); }
}

.input.error {
  border-color: #ef4444;
  animation: shake 400ms ease-in-out;
}

JavaScript:

function shakeElement(element) {
  element.classList.add('error');

  element.animate([
    { transform: 'translateX(0)' },
    { transform: 'translateX(-4px)' },
    { transform: 'translateX(4px)' },
    { transform: 'translateX(-4px)' },
    { transform: 'translateX(4px)' },
    { transform: 'translateX(-2px)' },
    { transform: 'translateX(2px)' },
    { transform: 'translateX(0)' }
  ], {
    duration: 400,
    easing: 'ease-in-out'
  });
}

Success Feedback

Confirmation animation.

CSS:

@keyframes success-pop {
  0% {
    transform: scale(0);
    opacity: 0;
  }
  50% {
    transform: scale(1.2);
  }
  100% {
    transform: scale(1);
    opacity: 1;
  }
}

.success-icon {
  animation: success-pop 400ms cubic-bezier(0.34, 1.56, 0.64, 1);
}

Complete success state:

function showSuccess(container) {
  const icon = container.querySelector('.success-icon');
  const message = container.querySelector('.success-message');

  // Icon pop
  icon.animate([
    { transform: 'scale(0)', opacity: 0 },
    { transform: 'scale(1.2)', opacity: 1 },
    { transform: 'scale(1)', opacity: 1 }
  ], {
    duration: 400,
    easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
    fill: 'forwards'
  });

  // Message fade in
  message.animate([
    { opacity: 0, transform: 'translateY(10px)' },
    { opacity: 1, transform: 'translateY(0)' }
  ], {
    duration: 300,
    delay: 200,
    easing: 'ease-out',
    fill: 'forwards'
  });
}

Tooltip Animation

Smooth tooltip appearance.

CSS:

.tooltip {
  position: absolute;
  opacity: 0;
  transform: translateY(4px);
  transition: opacity 150ms, transform 150ms;
  pointer-events: none;
}

.tooltip.visible {
  opacity: 1;
  transform: translateY(0);
}

/* With delay for hover stability */
[data-tooltip]:hover .tooltip {
  opacity: 1;
  transform: translateY(0);
  transition-delay: 200ms;
}

Framer Motion:

<AnimatePresence>
  {isVisible && (
    <motion.div
      className="tooltip"
      initial={{ opacity: 0, y: 4, scale: 0.95 }}
      animate={{ opacity: 1, y: 0, scale: 1 }}
      exit={{ opacity: 0, y: 4, scale: 0.95 }}
      transition={{ duration: 0.15 }}
    >
      {content}
    </motion.div>
  )}
</AnimatePresence>

Hover Lift

Card lift on hover.

CSS:

.card {
  transition: transform 200ms ease-out, box-shadow 200ms ease-out;
}

.card:hover {
  transform: translateY(-4px);
  box-shadow:
    0 4px 6px rgba(0, 0, 0, 0.07),
    0 12px 24px rgba(0, 0, 0, 0.1);
}

Loading Button

Button with loading state.

CSS:

.button-loading {
  position: relative;
  color: transparent;
}

.button-loading::after {
  content: '';
  position: absolute;
  width: 16px;
  height: 16px;
  top: 50%;
  left: 50%;
  margin: -8px 0 0 -8px;
  border: 2px solid transparent;
  border-top-color: white;
  border-radius: 50%;
  animation: spin 600ms linear infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

Reduced Motion

For prefers-reduced-motion: reduce:

Full MotionReduced Alternative
Button scaleOpacity change
Ripple effectInstant highlight
Toggle slideInstant state change
ShakeBorder color change
Tooltip animationInstant show/hide

Implementation:

@media (prefers-reduced-motion: reduce) {
  .button {
    transition: opacity 100ms;
  }

  .button:active {
    transform: none;
    opacity: 0.8;
  }

  .toggle-handle {
    transition: none;
  }

  .input.error {
    animation: none;
  }
}

Cross-References

  • See easing/ for spring configurations
  • See duration/ for timing standards
  • See component-animations/ for larger patterns
  • See accessibility/ for reduced motion

References

  • references/micro-patterns.md — Common micro-interaction patterns

دانلود ZIP