CSS gir deg to verktøy for bevegelse: transitions og @keyframes-animasjoner. De fleste utviklere vet at begge eksisterer, men bruker det de lærte først — vanligvis transitions — og lurer deretter på hvorfor lastingsspinneren ikke looper eller hvorfor varslingen snapper tilbake til sin opprinnelige posisjon. Beslutningen er ikke komplisert når du forstår hva hvert av dem egentlig er til for. Transitions er en reaksjon: noe endrer tilstand, nettleseren jevner ut endringen. @keyframes-animasjoner er selvstendige skript: de kjører på egenhånd, looper hvis du vil, og trenger ingen tilstandsutløser. Denne guiden dekker begge, egenskapene som betyr mest, og ytelsesdetaljene som skiller jevne 60fps-animasjoner fra hakke-fylte. Hvis du skriver mye CSS, er CSS Formatter verdt å bokmerke — det rydder raskt opp i rotete shorthand.
CSS Transitions — det enkle tilfellet
En transition forteller nettleseren: "når denne egenskapen endres, animer endringen over tid i stedet for å hoppe." Det er det. Den vanligste bruken er hover-effekter — fargeendringer, opasitetsfader, subtile skala-transformasjoner. MDN:s transitions-guide dekker hele spesifikasjonen, men de fire egenskapene du bryr deg om er transition-property, transition-duration, transition-timing-function og transition-delay.
/* 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 liste opp flere transitions adskilt med kommaer, hver med sin egen varighet og easing — det er hva knapp-eksemplet ovenfor gjør. Hver egenskap animerer uavhengig.
transition: all. Det er fristende som en catch-all, men det betyr at hver egenskapsendring på det elementet animeres — inkludert de du ikke hadde til hensikt. Det gir også nettleseren mer arbeid på hver frame. List opp de spesifikke egenskapene du vil transitionere. MDN:s transition-property-dokumentasjon forklarer hvilke egenskaper som kan animeres.transition-timing-function — easing forklart
Timing-funksjonen styrer animasjonens akselerasjonskurve — om den starter raskt og bremser ned, beveger seg med konstant hastighet, eller spretter gjennom en egendefinert kurve. De innebygde nøkkelordene dekker de fleste situasjoner.
ease— starter sakte, akselererer, deretter bremser ned mot slutten. Standard. Bra for de fleste UI-bevegelser.linear— konstant hastighet hele veien. Bra for spinnere og fremdriftslinjer, dårlig for de fleste andre ting — ser mekanisk ut.ease-in— starter sakte, slutter raskt. Føles som at noe tar fart. Bra for elementer som forlater skjermen.ease-out— starter raskt, slutter sakte. Føles naturlig for elementer som kommer inn på skjermen (som et varsel som glir inn).ease-in-out— sakte i begge ender. Ser polert ut for elementer som starter og stopper i synet.cubic-bezier(x1, y1, x2, y2)— definer din egen kurve. Verktøy som cubic-bezier.com lar deg bygge og forhåndsvise egendefinerte kurver visuelt.
/* 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 ikke er nok
Transitions har en hard grense: de trenger to tilstander og en utløser. Hvis du vil at noe skal loope uendelig, kjøre ved sideinnlasting uten noen interaksjon, eller animere gjennom mer enn to trinn, trenger du @keyframes. MDN:s @keyframes-referanse dekker hele syntaksen. Du definerer keyframes separat og fester dem til et element med egenskapen animation.
/* 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 — alle åtte egenskapene
Shorthand-egenskapen animation pakker åtte egenskaper inn i én deklarasjon. Du trenger dem ikke alle hver gang, men å kjenne hva hver enkelt styrer, sparer deg fra å grave i dokumentasjonen når noe ikke oppfører seg.
/*
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 — løsning på snapback-problemet
Dette er egenskapen som snubler alle på et tidspunkt. Som standard, når en animasjon slutter, snappes elementet tilbake til stilene fra før animasjonen — som om animasjonen aldri skjedde. Hvis du animerer en toast-varsling som glir inn fra høyre, snappes den tilbake utenfor skjermen i det øyeblikket animasjonen slutter. animation-fill-mode: forwards fikser dette ved å holde elementet ved sin endelige keyframe-tilstand når animasjonen er ferdig.
@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
*/Virkelige eksempler — skeleton-laster, spinner, toast, pulserende badge
Her er fire mønstre du vil bruke hele tiden. Hvert enkelt demonstrerer en spesifikk kombinasjon av @keyframes og animasjonsegenskaper.
/* --- 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%; }/* --- 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;
}/* --- 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;
}/* --- 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;
}Ytelse — GPU-kompositorerte vs. layout-utløsende egenskaper
Ikke alle CSS-egenskaper er like når det gjelder animasjonsytelse. Nettleserens rendering-pipeline har tre stadier som betyr noe her: layout (beregning av elementstørrelser og posisjoner), paint (fylle inn piksler) og composite (kombinere lag på GPU). Å animere en egenskap som utløser layout betyr at nettleseren beregner hele dokumentgeometrien på nytt på hver frame — det er dyrt, og det er det som forårsaker hakking. web.dev:s animasjonsytelses-guide dekker dette i dybden.
- Trygge å animere (GPU-kompositorerte):
transform(translate, scale, rotate) ogopacity. Disse kjører utelukkende på GPU-kompositortråden — de utløser aldri layout eller paint. - Forårsaker paint (unngå om mulig):
color,background-color,border-color,box-shadow. Disse hopper over layout, men utløser en repaint på hver frame. Korte transitions (under 300ms) er vanligvis OK. - Forårsaker layout (animer aldri):
width,height,top,left,margin,padding. Hver frame utløser en full layoutomberegning — garantert hakking på komplekse sider.
transform og opacity. Hvis du vil flytte noe, bruk translate ikke left/top. Hvis du vil endre størrelsen på noe, bruk scale ikke width/height. CSS Triggers er en referanse som viser nøyaktig hvilke renderingsstadier hver egenskap treffer./* 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 — bruk sparsomt
will-change er et hint til nettleseren om at en spesifikk egenskap snart vil bli animert, så den bør forfremme elementet til sitt eget kompositorlag på forhånd. Dette kan eliminere den korte hakken du noen ganger ser i starten av en animasjon på lav-end maskinvare. Men det har en reell kostnad: hvert forfremmet lag forbruker GPU-minne. Hvis du setter will-change: transform på hvert animert element i appen din, forringer du ytelsen på de fleste enheter — det motsatte av det du ønsket.
/* 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 — tilgjengelighet du ikke kan hoppe over
En betydelig del av brukerne har vestibulære forstyrrelser eller andre tilstander der bevegelse utløser ubehag eller kvalme. Mediaspørringen prefers-reduced-motion lar deg respektere systemnivå-innstillingen "reduser bevegelse". WCAG 2.1-retningslinje 2.3.3 dekker dette kravet, og MDN:s prefers-reduced-motion-referanse viser nettleserstøtte (det er universelt nå). Mønsteret er enkelt: pakk animasjonene dine inn i standard-mediaspørringen og gi et bevegelsefritt alternativ inne i den reduserte bevegelse-varianten.
/* 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 "nukleare alternativet" på slutten av det kodestykket er et vanlig mønster for eksisterende kodebaser som ikke har gjennomgått hver animasjon. Det er bedre enn ingenting, men å gå gjennom hver animasjon individuelt gir deg mer kontroll — noen animasjoner formidler tilstand (en fremdriftslinje, en lastingsspinner) og bør beholdes, bare settes ned i tempo.
Transitions vs @keyframes — beslutningsguiden
Hvis du noen gang er usikker på hvilken du skal bruke, still deg to spørsmål om animasjonen du bygger:
- Utløses den av en tilstandsendring? (hover, fokus, klassebytte, avkrysningsboks) → Bruk en transition. Det er akkurat hva transitions er designet for.
- Looper den uendelig? → Bruk @keyframes med
animation-iteration-count: infinite. - Har den mer enn to trinn? (ikke bare start → slutt, men start → midten → slutt eller mer) → Bruk @keyframes.
- Må den kjøre ved sideinnlasting, uten noen brukerinteraksjon? → Bruk @keyframes.
- Må elementet forbli ved sin slutningstilstand etter fullføring? → Bruk @keyframes med
animation-fill-mode: forwards.
Oppsummering
Transitions håndterer tilstandsdrevet bevegelse. @keyframes håndterer alt annet. Animer alltid transform og opacity for ytelse — la width, height, top og left være utenfor. Bruk animation-fill-mode: forwards når du trenger elementet til å holde sin slutningstilstand. Legg til prefers-reduced-motion-overriden før du sender noe — det er et ti-linjes tillegg som betyr mye for virkelige brukere. For mer om kompositeringsmodellen og hva som utløser layout, er web.dev:s rendering-ytelse-dokumentasjon den mest praktiske tilgjengelige ressursen. Når du har skrevet CSS-en, kjør den gjennom CSS Formatter for å holde shorthand-reglene lesbare, eller gjennom CSS Minifier for å fjerne mellomrom før levering til produksjon.