~/wiki / motion-i-animatsiya / css-animatsii-chto-umeet-brauzer

CSS animations without libraries: what a browser can do - and this is surprisingly much

Main chat

A chat for vibe coders: news, guides, live cases, marketplace, and finding executors.

$ cd section/ $ join vibe dev
CSS animations without libraries: what a browser can do - and this is surprisingly much - обложка

Basis: transitions and keyframes

Everyone already knows this, but it’s worth refreshing – because that’s what everything else is built on.

**Transitions ** The smooth change of one state to another when a class or pseudoclass is changed

css
.button {
  background: #1a1a2e;
  transform: scale(1);
  transition: background 0.2s ease, transform 0.15s ease;
}

.button:hover {
  background: #e94560;
  transform: scale(1.03);
}

A rule that saves a lot of time: animate only transform and opacity. They run on the GPU and do not cause reflow. top, left, width, height are all expensive and noticeable on mobile devices.

Keyframes - animation by key frames, run automatically or by class:

css
@keyframes slideUp {
  from {
    opacity: 0;
    transform: translateY(16px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.card {
  animation: slideUp 0.4s ease forwards;
}

forwards is an important parameter: without it, the element will return to its original state after the animation is over.


@starting-style - Emergence of elements without flash

Previously, if you had to animate the appearance of an element when adding to the DOM, it was JavaScript: add a class through requestAnimationFrame, otherwise the browser does not see the difference between the initial and final state.

@starting-style solves this in pure CSS. It specifies the starting values of properties when the element appears on the page:

css
.toast {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.3s ease, transform 0.3s ease,
              display 0.3s allow-discrete;
}

@starting-style {
  .toast {
    opacity: 0;
    transform: translateY(12px);
  }
}

The element appears - animated from @starting-style to the main values. Smooth, no JS, no setTimeout.

Cross-browser baseline reached in 2026 – Safari, Firefox, Chrome support.


display: none with output animation

The oldest pain in CSS: you can’t animate the disappearance of an element – display: none turns it off instantly before the transition has time to play. The standard solution is setTimeout for animation length. Fragile and unreliable.

Now it works through transition-behavior: allow-discrete:

css
.dropdown {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.2s ease, transform 0.2s ease,
              display 0.2s allow-discrete;
}

.dropdown[hidden] {
  display: none;
  opacity: 0;
  transform: translateY(-8px);
}

@starting-style {
  .dropdown {
    opacity: 0;
    transform: translateY(-8px);
  }
}

allow-discrete tells the browser: wait until the transition is complete, then switch the discrete property (display). It works for toasts, dropdowns, tultipes—any element that comes and goes.


interpolate-sizeheight: auto Animated

An accordion that unfolds smoothly is one of the most frequent requests. Previously, it was necessary to measure scrollHeight in JavaScript and animate to a specific value in pixels. It broke when the content changed.

One line in :root changes this:

css
:root {
  interpolate-size: allow-keywords;
}

.panel {
  height: 0;
  overflow: clip;
  transition: height 0.35s ease;
}

.panel[data-open] {
  height: auto;
}

interpolate-size: allow-keywords allows the browser to interpolate to keywords: auto, min-content, fit-content. This property is inherited, so one ad on :root is enough for the entire project.

Important: overflow: clip instead of overflow: hiddenclip does not create a random scroll container that interferes with height animation.


Scroll-driven animations - scrolling as a timeline

It's the most powerful thing that's ever happened. Animation progresses not in time but in scroll position. No JavaScript, no IntersectionObserver, no GSAP for basic scroll effects.

Two types of timeline:

**scroll()* - progress depends on the position in the scroll container. Ideal for progress bar or parallax background:

css
@keyframes progressBar {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

.reading-progress {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 3px;
  background: #e94560;
  transform-origin: left;
  animation: progressBar linear;
  animation-timeline: scroll(root);
}

Read progress strip without a single line of JS. Reacts to scrolling smoothly, works on compositor thread.

**view()* - progress depends on the position of the item in the eyeport. Ideal for reveal effects when scrolling:

css
@keyframes fadeInUp {
  from {
    opacity: 0;
    transform: translateY(24px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.section {
  animation: fadeInUp linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 30%;
}

animation-range: entry 0% entry 30% - Animation plays while the element enters the eyeport, occupying the first 30% of this input. After that, it remains in its final state.

Support in 2026 is cross-browser. Firefox and Safari closed support in 2025-2026, without prefixes.


Scroll-triggered animations – run on the scroll threshold

In Chrome 145 appeared separate from scroll-driven: animation-trigger. This is not a timeline where the animation is scrolled, this is a one-time run when the element crosses a predetermined threshold. Goodbye to IntersectionObserver for this pattern:

css
@keyframes popIn {
  from { opacity: 0; scale: 0.9; }
  to   { opacity: 1; scale: 1; }
}

.card {
  animation: popIn 0.4s ease forwards paused;
  animation-trigger: view(block 20%);
}

paused - Animation awaits. animation-trigger: view(block 20%): Starts when an element has entered 20% of the present port. Once, it won't.

As of June 2026, Chrome 145+ is available. Firefox and Safari are in progress. Add @supports:

css
@supports (animation-trigger: view()) {
  .card { animation-trigger: view(block 20%); }
}

@property - animation of gradients and custom values

Gradients in CSS cannot be animated via transition – the browser does not understand how to interpolate between two linear-gradient(). The workaround was through pseudoelements with opacity. @property solves this directly by registering a custom property with the type:

css
@property --gradient-angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

.card {
  background: linear-gradient(var(--gradient-angle), #1a1a2e, #e94560);
  transition: --gradient-angle 0.6s ease;
}

.card:hover {
  --gradient-angle: 180deg;
}

Now, with hover, the angle of the gradient is smoothly rotated - without JS, without pseudoelements. The same works for animating colors within the gradient, shadows, and any values that have not been transitionable before.

@property also opens animations of counters, color components and any numerical CSS variables. Support is cross-browser.


View Transitions – Transitions between pages

document.startViewTransition() is one of the most impressive APIs that have appeared in recent years. Smooth transition between DOM states or entire pages:

javascript
document.startViewTransition(() => {
  updateDOM(); // любое изменение DOM
});

By default, the browser does crossfade. Customization through CSS:

css
/* Переход между страницами — slide */
::view-transition-old(root) {
  animation: slide-out 0.3s ease forwards;
}
::view-transition-new(root) {
  animation: slide-in 0.3s ease forwards;
}

@keyframes slide-out {
  to { transform: translateX(-100%); }
}
@keyframes slide-in {
  from { transform: translateX(100%); }
}

For multi-page sites, automatic transitions through @view-transition { navigation: auto; } to CSS without JavaScript at all.


prefers-reduced-motion is mandatory

All animations above need to be wrapped in check. Some users disable animations in system settings for medical reasons or personal preferences:

css
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
  }
}

One rule at the end of global CSS is that all animations degrade to instantaneous transitions in those who disable them.


AI helps: Prompts for specific tasks

All the code above is perfectly generated by agents – CSS is well represented in the training data and new properties are also picked up. Three prompts for frequent tasks:

Reveal animations with scrolling without JS:

markdown
Add appearance animations when scrolling for all elements
with a .reveal class on the page.

Use only CSS: animation-timeline: view() and animation-range.
Animation: opacity 0→1, translateY 20px→0, duration 0.5s ease.
Do not use JavaScript, IntersectionObserver or third-party libraries.
Add @media (prefers-reduced-motion: reduce) with animations turned off.

Accordion with smooth opening to height: auto:

markdown
Create an accordion component in pure HTML and CSS.
Requirements:
Open/Close smoothly animates height to auto without JS measurements
Use interpolate-size: allow-keywords in :root
Animate the appearance of content through @starting-style
Switch only to CSS :has() or details/summary without JavaScript
Support for prefer-reduced-motion

Change between pages via View Transitions:

markdown
Add smooth transitions when navigating between pages
Next.js/React Router project.

Use the View Transitions API: document.startViewTransition().
Transition: The current page goes to the left, the new one comes to the right.
CSS for::view-transition-old and :view-transition-new.
Add Fallback: If the API is not supported, it is normal navigation without animation.
Do not use Framer Motion or other libraries for this effect.

When do you need a library

CSS doesn't replace everything. Three scenarios where the library is justified:

Physics and springs – spring() easing in CSS is not yet standardized. If you want a bounce with real physics - React Spring or GSAP.

Complex scrubbed timelines - several animations synchronized on one timeline with pauses, repetitions and dynamic control - GSAP ScrollTrigger.

Interactive states with gestures – swipe, drag, pinch – are always JavaScript. CSS does not handle gesture events.

Everything else is the browser first.

$ cd ../ ← back to Motion and animation