CSS ger dig två verktyg för rörelse: transitions och @keyframes-animationer. De flesta utvecklare vet att båda finns men sträcker sig efter det de lärde sig först — vanligtvis transitions — och undrar sedan varför laddningsspinnern inte loopar eller varför notifikationen snappar tillbaka till sin ursprungliga position. Beslutet är inte komplicerat när du förstår vad vart och ett egentligen är till för. Transitions är en reaktion: något ändrar tillstånd, webbläsaren jämnar ut förändringen. @keyframes-animationer är fristående skript: de körs på egen hand, loopar om du vill och behöver ingen tillståndsutlösare. Den här guiden täcker båda, de egenskaper som spelar störst roll, och prestandadetaljerna som separerar jämna 60fps-animationer från hackiga. Om du skriver mycket CSS är CSS Formatter värd att bokmärka — den rensar snabbt upp trasiga shorthand-regler.

CSS Transitions — det enkla fallet

En transition säger till webbläsaren: "när den här egenskapen ändras, animera förändringen över tid istället för att hoppa." Det är allt. Den vanligaste användningen är hover-effekter — färgförändringar, opacitetsfader, subtila skalningsomvandlingar. MDN:s transitions-guide täcker hela specifikationen, men de fyra egenskaperna du bryr dig om är transition-property, transition-duration, transition-timing-function och transition-delay.

css
/* The shorthand: property duration easing delay */
.btn-primary {
  background-color: #4f46e5;
  color: #fff;
  padding: 0.625rem 1.25rem;
  border-radius: 6px;
  border: none;
  cursor: pointer;
  transition: background-color 200ms ease, transform 150ms ease;
}

.btn-primary:hover {
  background-color: #4338ca;
  transform: translateY(-1px);
}

.btn-primary:active {
  transform: translateY(0);
}

/* Opacity fade — a card coming into view */
.notification-card {
  opacity: 0;
  transition: opacity 300ms ease-in-out;
}

.notification-card.is-visible {
  opacity: 1;
}

Du kan lista flera transitions separerade med kommatecken, var och en med sin egen varaktighet och easing — det är precis vad knappexemplet ovan gör. Varje egenskap animerar oberoende.

Använd inte transition: all. Det är frestande som en catch-all, men det innebär att varje egenskapsändring på det elementet animeras — inklusive de du inte avsåg. Det får också webbläsaren att göra mer arbete på varje bildruta. Lista de specifika egenskaperna du vill transitionera. MDN:s transition-property-dokumentation förklarar vilka egenskaper som är animerbara.

transition-timing-function — easing förklarad

Timing-funktionen styr animationens accelerationskurva — om den startar snabbt och saktar ner, rör sig i konstant takt, eller studsar genom en anpassad kurva. De inbyggda nyckelorden täcker de flesta situationer.

  • ease — startar långsamt, accelererar, sedan saktar ner nära slutet. Standard. Bra för de flesta UI-rörelser.
  • linear — konstant hastighet hela vägen. Bra för spinnrar och förloppsindikatorer, dåligt för de flesta andra saker — ser mekaniskt ut.
  • ease-in — startar långsamt, slutar snabbt. Känns som att något tar fart. Bra för element som lämnar skärmen.
  • ease-out — startar snabbt, slutar långsamt. Känns naturligt för element som kommer in på skärmen (som ett notifikationsmeddelande som glider in).
  • ease-in-out — långsamt i båda ändar. Ser polerat ut för element som startar och stannar i vy.
  • cubic-bezier(x1, y1, x2, y2) — definiera din egen kurva. Verktyg som cubic-bezier.com låter dig bygga och förhandsgranska anpassade kurvor visuellt.
css
/* Custom spring-like easing with cubic-bezier */
.drawer {
  transform: translateX(-100%);
  transition: transform 350ms cubic-bezier(0.34, 1.56, 0.64, 1);
}

.drawer.is-open {
  transform: translateX(0);
}

/* The cubic-bezier values above overshoot slightly (y > 1)
   which gives a satisfying spring effect on open */

@keyframes — när transitions inte räcker

Transitions har en hård gräns: de behöver två tillstånd och en utlösare. Om du vill att något ska loopa oändligt, köra vid sidladdning utan någon interaktion, eller animera genom mer än två steg behöver du @keyframes. MDN:s @keyframes-referens täcker hela syntaxen. Du definierar keyframes separat och fäster dem sedan till ett element med egenskapen animation.

css
/* Define the animation */
@keyframes spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

/* Attach it to an element */
.spinner {
  width: 24px;
  height: 24px;
  border: 3px solid #e5e7eb;
  border-top-color: #4f46e5;
  border-radius: 50%;
  animation: spin 700ms linear infinite;
}

/* Multi-step — more than two stops */
@keyframes pulse-ring {
  0%   { transform: scale(1);    opacity: 1; }
  50%  { transform: scale(1.15); opacity: 0.7; }
  100% { transform: scale(1);    opacity: 1; }
}

.badge-live {
  animation: pulse-ring 1.5s ease-in-out infinite;
}

animation-shorthand — alla åtta egenskaperna

Shorthand-egenskapen animation packar åtta egenskaper i en deklaration. Du behöver inte alla varje gång, men att känna till vad varje kontrollerar sparar dig från att gräva i dokumentationen när något inte beter sig.

css
/*
  animation: name | duration | timing-function | delay | iteration-count | direction | fill-mode | play-state
*/
.toast {
  animation: slide-in-right 350ms ease-out 0s 1 normal forwards running;
}

/* More commonly written as just the values you need: */
.toast {
  animation: slide-in-right 350ms ease-out forwards;
}

/* Breaking it out to individual properties for clarity: */
.confetti-piece {
  animation-name: float-down;
  animation-duration: 2s;
  animation-timing-function: ease-in;
  animation-delay: calc(var(--i) * 150ms); /* staggered via CSS custom property */
  animation-iteration-count: 1;
  animation-direction: normal;
  animation-fill-mode: forwards;
  animation-play-state: running;
}

/* Pausing an animation via JavaScript: toggle a class */
.spinner.is-paused {
  animation-play-state: paused;
}

animation-fill-mode — åtgärda snapback-problemet

Det här är egenskapen som snubblar alla vid något tillfälle. Som standard, när en animation slutar, snappar elementet tillbaka till sina stilar före animationen — som om animationen aldrig hände. Om du animerar en toast-notifikation som glider in från höger, snappar den tillbaka utanför skärmen i samma ögonblick animationen slutar. animation-fill-mode: forwards åtgärdar detta genom att hålla elementet vid sin slutliga keyframe-stil när animationen är klar.

css
@keyframes slide-in-right {
  from {
    transform: translateX(110%);
    opacity: 0;
  }
  to {
    transform: translateX(0);
    opacity: 1;
  }
}

/* Without forwards: toast slides in, then SNAPS back off-screen */
.toast-bad {
  animation: slide-in-right 350ms ease-out;
}

/* With forwards: toast slides in and STAYS in position */
.toast {
  animation: slide-in-right 350ms ease-out forwards;
}

/*
  fill-mode values:
  - none      (default) — no styles applied before or after
  - forwards  — hold the final keyframe after the animation ends
  - backwards — apply the first keyframe during the delay period
  - both      — forwards + backwards combined
*/

Verkliga exempel — skeletonladdare, spinner, toast, pulserande badge

Här är fyra mönster du kommer att använda hela tiden. Vart och ett demonstrerar en specifik kombination av @keyframes och animationsegenskaper.

css
/* --- 1. Skeleton loading shimmer --- */
@keyframes shimmer {
  0%   { background-position: -200% 0; }
  100% { background-position: 200% 0; }
}

.skeleton {
  background: linear-gradient(
    90deg,
    #f0f0f0 25%,
    #e0e0e0 50%,
    #f0f0f0 75%
  );
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite linear;
  border-radius: 4px;
}

.skeleton-title  { height: 20px; width: 60%; margin-bottom: 12px; }
.skeleton-text   { height: 14px; width: 100%; margin-bottom: 8px; }
.skeleton-text:last-child { width: 80%; }
css
/* --- 2. Spinning loader --- */
@keyframes spin {
  to { transform: rotate(360deg); }
}

.loader-spinner {
  display: inline-block;
  width: 32px;
  height: 32px;
  border: 3px solid rgba(79, 70, 229, 0.2);
  border-top-color: #4f46e5;
  border-radius: 50%;
  animation: spin 600ms linear infinite;
}
css
/* --- 3. Slide-in notification toast --- */
@keyframes toast-enter {
  from {
    transform: translateX(calc(100% + 1.5rem));
    opacity: 0;
  }
  to {
    transform: translateX(0);
    opacity: 1;
  }
}

@keyframes toast-exit {
  from {
    transform: translateX(0);
    opacity: 1;
  }
  to {
    transform: translateX(calc(100% + 1.5rem));
    opacity: 0;
  }
}

.toast {
  position: fixed;
  bottom: 1.5rem;
  right: 1.5rem;
  padding: 0.875rem 1.25rem;
  background: #1f2937;
  color: #fff;
  border-radius: 8px;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
  animation: toast-enter 350ms ease-out forwards;
}

.toast.is-dismissing {
  animation: toast-exit 300ms ease-in forwards;
}
css
/* --- 4. Pulsing notification badge --- */
@keyframes badge-pulse {
  0%, 100% {
    box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.5);
  }
  50% {
    box-shadow: 0 0 0 6px rgba(239, 68, 68, 0);
  }
}

.notification-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 20px;
  height: 20px;
  padding: 0 5px;
  background: #ef4444;
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  border-radius: 999px;
  animation: badge-pulse 2s ease-in-out infinite;
}

Prestanda — GPU-kompositorerade vs. layoututlösande egenskaper

Alla CSS-egenskaper är inte lika när det gäller animationsprestanda. Webbläsarens renderingspipeline har tre stadier som spelar roll här: layout (beräkning av elementstorlekar och positioner), paint (fylla i pixlar) och composite (kombinera lager på GPU). Att animera en egenskap som utlöser layout innebär att webbläsaren beräknar om hela dokumentgeometrin på varje bildruta — det är dyrt och orsaker hackighet. web.dev:s animationsprestandaguide täcker detta ingående.

  • Säkra att animera (GPU-kompositorerade): transform (translate, scale, rotate) och opacity. Dessa körs helt på GPU-kompositortråden — de utlöser aldrig layout eller paint.
  • Orsakar paint (undvik om möjligt): color, background-color, border-color, box-shadow. Dessa hoppar över layout men utlöser en repaint på varje bildruta. Korta transitions (under 300ms) är vanligtvis okej.
  • Orsakar layout (animera aldrig): width, height, top, left, margin, padding. Varje bildruta utlöser en fullständig layoutomberäkning — garanterad hackighet på komplexa sidor.
Den praktiska regeln: animera med bara transform och opacity. Om du vill flytta något, använd translate inte left/top. Om du vill ändra storlek på något, använd scale inte width/height. CSS Triggers är en referens som listar exakt vilka renderingsstadier varje egenskap träffar.
css
/* Bad — animating width triggers layout on every frame */
.progress-bar-bad {
  transition: width 300ms ease;
}

/* Good — use scaleX transform instead */
.progress-bar {
  transform-origin: left center;
  transition: transform 300ms ease;
}

/* In JS, set: progressBar.style.transform = 'scaleX(0.75)' for 75% */

/* Bad — animating top/left (positions element with layout) */
.tooltip-bad {
  position: absolute;
  top: 0;
  transition: top 200ms ease;
}

/* Good — use translateY instead */
.tooltip {
  position: absolute;
  top: 0;
  transform: translateY(0);
  transition: transform 200ms ease;
}

will-change — använd sparsamt

will-change är en ledtråd till webbläsaren om att en specifik egenskap snart kommer att animeras, så den bör lyfta fram elementet till sitt eget kompositorlager i förväg. Detta kan eliminera den korta hackningstegnet du ibland ser i början av en animation på lågkonfigurerad hårdvara. Men det har en verklig kostnad: varje framlyft lager förbrukar GPU-minne. Om du sätter will-change: transform på varje animerat element i din app försämrar du prestandan på de flesta enheter — raka motsatsen till vad du ville.

css
/* Right way — add it just before animation starts, remove after */
.modal-overlay {
  /* Don't set will-change here by default */
}

/* Add via JavaScript only when user triggers modal open */
/* overlay.style.willChange = 'opacity'; */
/* overlay.addEventListener('transitionend', () => { */
/*   overlay.style.willChange = 'auto'; */
/* }); */

/* Acceptable in CSS for elements that animate frequently
   (e.g., a persistent floating action button on scroll) */
.fab {
  will-change: transform; /* OK — this element genuinely animates on scroll */
  transition: transform 200ms ease;
}

/* Don't do this — wastes GPU memory with zero benefit */
.card {
  will-change: transform; /* BAD — card only animates on hover, not constantly */
}

prefers-reduced-motion — tillgänglighet du inte kan hoppa över

En betydande del av användarna har vestibulära störningar eller andra tillstånd där rörelse orsakar obehag eller illamående. Mediafrågan prefers-reduced-motion låter dig respektera systemnivåinställningen "minska rörelse". WCAG 2.1-riktlinje 2.3.3 täcker det här kravet, och MDN:s prefers-reduced-motion-referens visar webbläsarstöd (det är universellt nu). Mönstret är enkelt: wrappa dina animationer i standardmediafrågan och tillhandahåll ett rörelsefritt alternativ inuti den reducerade rörelse-varianten.

css
/* Define animations normally for users who are fine with motion */
@keyframes slide-in-right {
  from { transform: translateX(110%); opacity: 0; }
  to   { transform: translateX(0);    opacity: 1; }
}

.toast {
  animation: slide-in-right 350ms ease-out forwards;
}

.spinner {
  animation: spin 600ms linear infinite;
}

/* Override for users who prefer reduced motion */
@media (prefers-reduced-motion: reduce) {
  .toast {
    /* No sliding — just appear */
    animation: none;
    opacity: 1;
    transform: none;
  }

  .spinner {
    /* Slow it way down or stop it entirely */
    animation-duration: 4s;
  }

  /* Nuclear option — disable ALL animations site-wide */
  /* Use this only if you haven't audited each animation individually */
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

Det "nukleära alternativet" i slutet av det kodfragmentet är ett vanligt mönster för befintliga kodbaser som inte har granskat varje animation. Det är bättre än ingenting, men att granska varje animation individuellt ger dig mer kontroll — vissa animationer förmedlar tillstånd (en förloppslinje, en laddningsspinnrar) och bör behållas, bara saktas ner.

Transitions vs @keyframes — beslutsguiden

Om du någonsin är osäker på vilket du ska använda, ställ dig två frågor om animationen du bygger:

  • Utlöses den av en tillståndsändring? (hover, fokus, klassbyte, checkbox) → Använd en transition. Det är precis vad transitions är designade för.
  • Loopar den oändligt? → Använd @keyframes med animation-iteration-count: infinite.
  • Har den mer än två steg? (inte bara start → slut, utan start → mitten → slut eller mer) → Använd @keyframes.
  • Behöver den köra vid sidladdning, utan någon användarinteraktion? → Använd @keyframes.
  • Behöver elementet stanna kvar i sitt slutläge efter att animationen är klar? → Använd @keyframes med animation-fill-mode: forwards.
Kombinationen som fångar folk: du vill ha en hover-effekt som jämnt tonar ett bakgrundsfärg (transition), men du vill också ha en "ny"-badge på samma element som pulserar kontinuerligt (@keyframes). Du kan använda båda på samma element — de kontrollerar olika egenskaper och stör inte varandra.

Sammanfattning

Transitions hanterar tillståndsdriven rörelse. @keyframes hanterar allt annat. Animera alltid transform och opacity för prestanda — lämna width, height, top och left utanför. Använd animation-fill-mode: forwards när du behöver elementet att hålla kvar sitt slutläge. Lägg till prefers-reduced-motion-overriden innan du skickar något — det är ett tioradigt tillägg som spelar stor roll för verkliga användare. För mer om kompositeringsmodellen och vad som utlöser layout är web.dev:s renderingsprestanda-dokumentation den mest praktiska tillgängliga resursen. När du har skrivit CSS:en, kör den genom CSS Formatter för att hålla shorthand-reglerna läsbara, eller genom CSS Minifier för att ta bort blanksteg innan du skickar till produktion.