feat: micro-interactions and animations — cards, grids, tables, modals, nav
CSS-only animation layer (zero component changes): - Staggered grid item entrance (fadeInUp with 30ms delays per item) - Staggered table row entrance (fadeIn with 25ms delays) - Card hover lift in grids (translateY -2px + elevated shadow) - Sidebar nav: active link accent bar, hover slide-right - Badge hover scale, status badge pop-in - Modal backdrop fade + panel pop-in (fadeInScale) - Floating action bar slide-up entrance - Image/thumbnail zoom on group hover (scale 1.03) - Number/stat counter entrance animation - Checkbox pop-in on check - Link animated underline (background-size transition) - Select hover border highlight - Loading skeleton shimmer gradient - Smooth scroll behavior - prefers-reduced-motion: respects user preference Keyframes: fadeIn, fadeInUp, fadeInScale, slideInRight, slideInLeft, slideUp, popIn, breathe, countUp, shimmer, wiggle Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+317
-14
@@ -350,31 +350,334 @@
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
UTILITY ENHANCEMENTS
|
||||
ANIMATIONS & MICRO-INTERACTIONS
|
||||
============================================================ */
|
||||
|
||||
/* Smooth page transitions */
|
||||
.page-enter {
|
||||
animation: fadeIn 200ms ease-out;
|
||||
}
|
||||
/* ── Keyframes ─────────────────────────────────────────────── */
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(4px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
from { opacity: 0; transform: translateY(6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(12px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes fadeInScale {
|
||||
from { opacity: 0; transform: scale(0.96); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from { opacity: 0; transform: translateX(16px); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
}
|
||||
|
||||
@keyframes slideInLeft {
|
||||
from { opacity: 0; transform: translateX(-16px); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* Pulse animation for loading states */
|
||||
@keyframes subtlePulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
}
|
||||
|
||||
@keyframes progressGrow {
|
||||
from { width: 0%; }
|
||||
}
|
||||
|
||||
@keyframes popIn {
|
||||
0% { opacity: 0; transform: scale(0.8); }
|
||||
70% { transform: scale(1.02); }
|
||||
100% { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
@keyframes breathe {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(var(--accent-rgb, 0, 137, 61), 0.15); }
|
||||
50% { box-shadow: 0 0 0 6px rgba(var(--accent-rgb, 0, 137, 61), 0); }
|
||||
}
|
||||
|
||||
@keyframes countUp {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* ── Utility animation classes ─────────────────────────────── */
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 300ms ease-out both;
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
animation: fadeInUp 400ms ease-out both;
|
||||
}
|
||||
|
||||
.animate-fade-in-scale {
|
||||
animation: fadeInScale 250ms ease-out both;
|
||||
}
|
||||
|
||||
.animate-slide-in-right {
|
||||
animation: slideInRight 300ms ease-out both;
|
||||
}
|
||||
|
||||
.animate-slide-in-left {
|
||||
animation: slideInLeft 300ms ease-out both;
|
||||
}
|
||||
|
||||
.animate-slide-up {
|
||||
animation: slideUp 350ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
|
||||
.animate-pop-in {
|
||||
animation: popIn 300ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
|
||||
.animate-subtle-pulse {
|
||||
animation: subtlePulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-breathe {
|
||||
animation: breathe 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-count-up {
|
||||
animation: countUp 400ms ease-out both;
|
||||
}
|
||||
|
||||
/* ── Card hover micro-interaction ──────────────────────────── */
|
||||
|
||||
.card {
|
||||
transition: transform 200ms ease, box-shadow 200ms ease, border-color 200ms ease;
|
||||
}
|
||||
.card:hover {
|
||||
/* Subtle lift + glow on hover — only for non-table cards */
|
||||
}
|
||||
|
||||
/* Cards in grid layouts get the hover lift */
|
||||
.grid > .card:hover,
|
||||
.card.hover-lift:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px -4px rgba(0, 0, 0, 0.15), 0 4px 8px -2px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
:root.dark .grid > .card:hover,
|
||||
:root.dark .card.hover-lift:hover {
|
||||
box-shadow: 0 8px 24px -4px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
/* ── Staggered grid item entrance ──────────────────────────── */
|
||||
|
||||
.grid > * {
|
||||
animation: fadeInUp 350ms ease-out both;
|
||||
}
|
||||
.grid > *:nth-child(1) { animation-delay: 0ms; }
|
||||
.grid > *:nth-child(2) { animation-delay: 30ms; }
|
||||
.grid > *:nth-child(3) { animation-delay: 60ms; }
|
||||
.grid > *:nth-child(4) { animation-delay: 90ms; }
|
||||
.grid > *:nth-child(5) { animation-delay: 120ms; }
|
||||
.grid > *:nth-child(6) { animation-delay: 150ms; }
|
||||
.grid > *:nth-child(7) { animation-delay: 180ms; }
|
||||
.grid > *:nth-child(8) { animation-delay: 210ms; }
|
||||
.grid > *:nth-child(9) { animation-delay: 240ms; }
|
||||
.grid > *:nth-child(10) { animation-delay: 270ms; }
|
||||
.grid > *:nth-child(11) { animation-delay: 300ms; }
|
||||
.grid > *:nth-child(12) { animation-delay: 330ms; }
|
||||
.grid > *:nth-child(n+13) { animation-delay: 350ms; }
|
||||
|
||||
/* ── Table row entrance ────────────────────────────────────── */
|
||||
|
||||
tbody > tr {
|
||||
animation: fadeIn 250ms ease-out both;
|
||||
}
|
||||
tbody > tr:nth-child(1) { animation-delay: 0ms; }
|
||||
tbody > tr:nth-child(2) { animation-delay: 25ms; }
|
||||
tbody > tr:nth-child(3) { animation-delay: 50ms; }
|
||||
tbody > tr:nth-child(4) { animation-delay: 75ms; }
|
||||
tbody > tr:nth-child(5) { animation-delay: 100ms; }
|
||||
tbody > tr:nth-child(n+6) { animation-delay: 120ms; }
|
||||
|
||||
/* ── Sidebar nav link animations ───────────────────────────── */
|
||||
|
||||
nav a, nav button {
|
||||
transition: all 180ms ease-out;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Active nav link: left accent bar */
|
||||
nav a.bg-accent-light {
|
||||
border-left: 3px solid var(--color-accent);
|
||||
padding-left: calc(0.75rem - 3px);
|
||||
}
|
||||
|
||||
/* Nav link hover: subtle slide right */
|
||||
nav a:not(.bg-accent-light):hover,
|
||||
nav button:hover {
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
/* ── Badge animations ──────────────────────────────────────── */
|
||||
|
||||
.badge, [class*="badge-"] {
|
||||
transition: all 150ms ease-out;
|
||||
}
|
||||
.badge:hover, [class*="badge-"]:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* Status badges in tables — subtle entrance */
|
||||
td .badge, td [class*="badge-"] {
|
||||
animation: popIn 250ms ease-out both;
|
||||
}
|
||||
|
||||
/* ── Progress bar animation ────────────────────────────────── */
|
||||
|
||||
[role="progressbar"] > div,
|
||||
.bg-green-500,
|
||||
.bg-red-500,
|
||||
.bg-amber-500,
|
||||
.bg-blue-500 {
|
||||
transition: width 600ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
/* Render progress bars */
|
||||
div[class*="bg-status-success"],
|
||||
div[class*="bg-green"] {
|
||||
transition: width 600ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
/* ── Modal / dialog animations ─────────────────────────────── */
|
||||
|
||||
/* Backdrop fade */
|
||||
.fixed.inset-0.z-50 {
|
||||
animation: fadeIn 200ms ease-out;
|
||||
}
|
||||
|
||||
/* Modal panel pop-in */
|
||||
.fixed.inset-0.z-50 > div[role="dialog"],
|
||||
.fixed.inset-0.z-50 > div:not([class*="absolute"]) > div {
|
||||
animation: fadeInScale 250ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
/* ── Floating action bar slide-up ──────────────────────────── */
|
||||
|
||||
.fixed.bottom-6,
|
||||
.fixed.bottom-0 {
|
||||
animation: slideUp 300ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
/* ── Toast notifications ───────────────────────────────────── */
|
||||
|
||||
[data-sonner-toaster] [data-sonner-toast] {
|
||||
animation: slideInRight 300ms cubic-bezier(0.16, 1, 0.3, 1) !important;
|
||||
}
|
||||
|
||||
/* ── Loading skeleton shimmer ──────────────────────────────── */
|
||||
|
||||
.animate-pulse {
|
||||
animation: subtlePulse 1.5s ease-in-out infinite !important;
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--color-bg-muted) 25%,
|
||||
var(--color-bg-surface-hover) 50%,
|
||||
var(--color-bg-muted) 75%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s ease-in-out infinite;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
/* ── Image/thumbnail hover zoom ────────────────────────────── */
|
||||
|
||||
.group img,
|
||||
.group video {
|
||||
transition: transform 300ms ease-out;
|
||||
}
|
||||
.group:hover img,
|
||||
.group:hover video {
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
/* ── Number/stat counter entrance ──────────────────────────── */
|
||||
|
||||
.text-2xl, .text-3xl, .text-4xl {
|
||||
animation: countUp 500ms ease-out both;
|
||||
}
|
||||
|
||||
/* ── Checkbox/toggle animation ─────────────────────────────── */
|
||||
|
||||
input[type="checkbox"] {
|
||||
transition: all 150ms ease-out;
|
||||
}
|
||||
input[type="checkbox"]:checked {
|
||||
animation: popIn 200ms ease-out;
|
||||
}
|
||||
|
||||
/* ── Link hover underline animation ────────────────────────── */
|
||||
|
||||
a.hover\:underline {
|
||||
text-decoration: none;
|
||||
background-image: linear-gradient(currentColor, currentColor);
|
||||
background-position: 0% 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 0% 1px;
|
||||
transition: background-size 200ms ease-out;
|
||||
}
|
||||
a.hover\:underline:hover {
|
||||
background-size: 100% 1px;
|
||||
}
|
||||
|
||||
/* ── Dropdown/select hover ─────────────────────────────────── */
|
||||
|
||||
select {
|
||||
transition: all 150ms ease-out;
|
||||
cursor: pointer;
|
||||
}
|
||||
select:hover {
|
||||
border-color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* ── Notification bell wiggle ──────────────────────────────── */
|
||||
|
||||
@keyframes wiggle {
|
||||
0%, 100% { transform: rotate(0deg); }
|
||||
25% { transform: rotate(8deg); }
|
||||
75% { transform: rotate(-8deg); }
|
||||
}
|
||||
|
||||
.notification-bell:hover {
|
||||
animation: wiggle 400ms ease-in-out;
|
||||
}
|
||||
|
||||
/* ── Smooth scroll behavior ────────────────────────────────── */
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* ── Reduced motion: respect user preference ───────────────── */
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user