WWDC Quick Look 💓 By SwiftGGTeam
What’s new in Safari and WebKit

What’s new in Safari and WebKit

Watch original video

Highlight

Safari 19 brings scroll-driven animations, cross-document view transitions, and anchor positioning. Scroll-linked animations, cross-page transitions, and popover placement can now be done in pure CSS.


Core content

To build a scroll progress bar, you used to listen for scroll events and change transform from JavaScript. Saron Yitbarek opens the session with the point: JavaScript can do a lot, but anything you can drop gives the user better performance and battery life (03:24). Safari 19 pushes this work down into CSS.

Using the “A-School of Code” site as the example, she walks through four new capabilities. For animation, scroll-driven animations bind CSS directly to scroll progress, and view transitions add smooth transitions between same-origin page loads. For layout, anchor positioning hands the placement math for popovers and menus over to the browser. For visuals, background-clip: border-area lets borders carry gradients, and the shape() function replaces path() for responsive shapes. For media, SVG favicons, HDR images, and Ogg Opus/Vorbis fill in the gaps.


Details

The smallest scroll-driven animation example (06:18). Turn the footer’s ::after pseudo-element into a progress bar and bind the animation to the scroll container with animation-timeline: scroll():

footer::after {
  content: "";
  height: 1em;
  width: 100%;
  background: var(--yellow);
  left: 0;
  bottom: 0;
  position: fixed;
  transform-origin: top left;
  animation: progress-scale linear;
  animation-timeline: scroll();
}

@keyframes progress-scale {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

Key points:

  • position: fixed plus bottom: 0 pins the bar to the bottom of the viewport.
  • transform-origin: top left makes the scaling start from the left.
  • animation: progress-scale linear declares a linear curve with no duration.
  • animation-timeline: scroll() swaps the timeline for the scroll progress of the nearest scroll container. It must come after animation to override the default timeline.
  • @keyframes is plain CSS, going from scaleX(0) to scaleX(1) to fill the bar across.

view() timeline plus animation-range (12:20). When you want the animation to fire as an element enters and leaves the viewport, use view() instead of scroll():

.topic-item {
  animation-fill-mode: both;
  animation-timeline: view();
  animation-range: 0% 50%;
  &:nth-child(3n + 1) { animation-name: in-from-left; }
  &:nth-child(3n + 2) { animation-name: in-from-middle; }
  &:nth-child(3n + 3) { animation-name: in-from-right; }
}

Key points:

  • animation-fill-mode: both keeps the start and end frame styles before and after the animation.
  • animation-timeline: view() ties progress to the element’s position relative to the viewport.
  • animation-range: 0% 50% limits the animation to the span between the element entering the viewport and reaching the halfway point. The element stays still and readable through the rest of the scroll (12:00).
  • :nth-child(3n + 1/2/3) sends the three columns to three different @keyframes for a staggered entrance.

Cross-document view transitions (14:20). For navigation between same-origin pages, one line is enough:

@view-transition {
  navigation: auto;
}

@media not (prefers-reduced-motion) {
  @keyframes slide-in {
    from { translate: 100vw 0; }
  }
  @keyframes slide-out {
    to { translate: -100vw 0; }
  }
}

Key points:

  • Add @view-transition { navigation: auto } to both pages and the browser fades same-origin navigations on its own.
  • Wrap the custom slide effect in @media not (prefers-reduced-motion) to respect the system “Reduce Motion” setting (16:00).
  • Give specific elements a view-transition-name, then use ::view-transition-old(...) and ::view-transition-new(...) to control their exit and entrance animations.

Anchor positioning (21:5823:25). Combined with the popover API, menus no longer need JS for placement:

.profile-button {
  anchor-name: --profile-button;
}

.profile-menu {
  position-anchor: --profile-button;
  position-area: top right;
}

Key points:

  • anchor-name: --profile-button registers the button as an anchor. The name is a CSS custom identifier.
  • position-anchor: --profile-button points the menu at the anchor and binds the two together.
  • position-area: top right uses a keyword to place the menu in the area above and to the right of the anchor. You can use bottom center, span-right, span-left, and others.
  • For finer control, switch to top: anchor(bottom); left: anchor(left). The anchor() function returns the coordinate of the matching edge of the anchor and can be wrapped in calc() (28:26).

border-area gradient borders (31:05):

.primary-btn {
  background: border-area linear-gradient(to bottom right in hsl, yellow, orange);
  border-color: transparent;
}

Key points:

  • The border-area keyword in the background shorthand extends the background image into the border area.
  • border-color: transparent clears the border’s own color so the gradient shows through.
  • This is the pure-CSS version of a gradient stroke that used to take a pseudo-element or SVG.

Takeaways

  1. Move the scroll progress bar back to CSS: For the progress bar at the bottom of a site or the top of an article, replace the scroll listener with animation-timeline: scroll(). The reason it pays off: drop the JS and the main thread stops waking up on scroll, which steadies frame rate and battery life on mobile. How to start: open the demo page in Safari Technology Preview, rewrite the existing progress-bar component as a position: fixed pseudo-element, then add one line of animation-timeline: scroll() to verify.

  2. Use view() plus animation-range for card entrances: For lists, card grids, and section headings in long documents, IntersectionObserver used to trigger the animation. The reason it pays off: view() feeds the element’s position relative to the viewport directly into animation progress, and the animation plays back naturally on reverse scroll. How to start: pick an existing IntersectionObserver entrance animation, drop the JS, switch to animation-timeline: view(), and set animation-range to 0% 50% so the animation only takes up the first half of the viewport and the content stays readable.

  3. Replace popper.js with anchor positioning: For every dropdown menu or tooltip that relies on popper.js or floating-ui to compute placement. The reason it pays off: anchor-name plus position-area plus position-try hands the flip logic to the browser, taking bundle size, bugs, and keyboard focus along with it. How to start: pick the simplest avatar menu, add popover and popovertarget in HTML, give the button an anchor-name and the menu a position-anchor with position-area: bottom center, then use @supports to keep an older path for older browsers.

  4. Treat prefers-reduced-motion as a hard constraint: Wrap every scroll-driven and view-transition rule in @media not (prefers-reduced-motion). The reason it pays off: Saron names motion discomfort in the session as a common symptom that includes real physical effects like vertigo and nausea. How to start: add a rule to your team’s CSS lint so any selector containing animation-timeline or @view-transition must sit inside a prefers-reduced-motion media query, otherwise warn.


Comments

GitHub Issues · utterances