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: fixedplusbottom: 0pins the bar to the bottom of the viewport.transform-origin: top leftmakes the scaling start from the left.animation: progress-scale lineardeclares a linear curve with no duration.animation-timeline: scroll()swaps the timeline for the scroll progress of the nearest scroll container. It must come afteranimationto override the default timeline.@keyframesis plain CSS, going fromscaleX(0)toscaleX(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: bothkeeps 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@keyframesfor 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:58 → 23: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-buttonregisters the button as an anchor. The name is a CSS custom identifier.position-anchor: --profile-buttonpoints the menu at the anchor and binds the two together.position-area: top rightuses a keyword to place the menu in the area above and to the right of the anchor. You can usebottom center,span-right,span-left, and others.- For finer control, switch to
top: anchor(bottom); left: anchor(left). Theanchor()function returns the coordinate of the matching edge of the anchor and can be wrapped incalc()(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-areakeyword in thebackgroundshorthand extends the background image into the border area. border-color: transparentclears 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
-
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 aposition: fixedpseudo-element, then add one line ofanimation-timeline: scroll()to verify. -
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 toanimation-timeline: view(), and setanimation-rangeto0% 50%so the animation only takes up the first half of the viewport and the content stays readable. -
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-nameplusposition-areaplusposition-tryhands the flip logic to the browser, taking bundle size, bugs, and keyboard focus along with it. How to start: pick the simplest avatar menu, addpopoverandpopovertargetin HTML, give the button ananchor-nameand the menu aposition-anchorwithposition-area: bottom center, then use@supportsto keep an older path for older browsers. -
Treat
prefers-reduced-motionas 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 containinganimation-timelineor@view-transitionmust sit inside aprefers-reduced-motionmedia query, otherwise warn.
Related sessions
- Discover Apple-Hosted Background Assets — Use Apple-hosted Background Assets to pre-download large resources before the user first launches the app.
- Dive deeper into Writing Tools — Wire Writing Tools into your text controls so proofreading, rewriting, and transforming happen at the system layer.
- Dive into App Store server APIs for In-App Purchase — The latest updates to App Store Server API and Notifications V2 for handling subscription and refund callbacks.
- Enhance child safety with PermissionKit — Use PermissionKit to add a parent-controlled safety gate for child accounts inside communication apps.
Comments
GitHub Issues · utterances