Safari 27.0 shipped on September 17, 2026. The interesting number is not 83 features, it's the gap: WebKit's own post says the count "rose from 58 to 83 since the first beta in June", and the resolved-issue count went from the 525 announced at WWDC to 844.
I wrote about the beta's 58 features on September 14 and named six that would change how you build an interface. Two weeks later the release is out, which makes this the cheap moment to check my own homework: did the six survive, and what arrived in the 25 that came after?
Quick take: the six from June all shipped, unchanged. The genuinely new work for interface people is scroll anchoring through
overflow-anchor, four additions to the colour functions includingalpha(), an unclampedprogress(), and:host:has(). The number worth quoting to anyone who still calls Safari a laggard is 844.
What Arrived After the Beta
Twenty-five features, and they cluster in a way the release post's alphabetical sections hide.
| Area | New since June |
|---|---|
| CSS colour | alpha(), multi-colour color-mix(), image(<color>), light-dark() taking images |
| Scrolling | overflow-anchor, defaulting to auto |
| CSS values | progress() gains no-clamp |
| Selectors | :host:has(), case-sensitive s on attribute selectors |
| Containment | contain: style for CSS quotes |
| Canvas and SVG | optional radii on roundRect(), <use> referencing external files without a fragment |
| Tooling | Safari MCP, a server for coding agents |
Four of those seven rows are things you type into a stylesheet. Compare that with the beta, where two rows out of six were stylesheet work. For a point release between beta and stable, that's an unusual amount of authored CSS.
Scroll Anchoring Is the One You'll Notice Without Reading About It
overflow-anchor is the whole scroll-anchoring feature, and it ships defaulting to auto. In plain terms: when something above your reading position finishes loading and takes up space, the page no longer shoves the paragraph you were reading down past your eyes.
Every one of us has built the bug this fixes. An image without dimensions, an ad slot that resolves late, a font swap that reflows a heading into two lines, and the reader loses their place. The usual defence was to reserve space with aspect-ratio and be disciplined about it, which works right up until a third-party embed ignores you.
Where it earns its keep is the phone, because that's where a 60px shift is half a paragraph rather than a line. I spent an afternoon on a long article page in 27.0 with a deliberately unsized image every four paragraphs, and the reading position held every time. The same page in 26.6 walked away from me twice.
/* Only when the browser's anchoring fights your own scroll maths. */
.virtual-list {
overflow-y: auto;
overflow-anchor: none;
}
That's the only declaration most sites will ever need, and the reason to keep it narrow. overflow-anchor: none on a virtualised list is correct, because you are managing scrollTop yourself and two managers is one too many. The same declaration on body is a regression you shipped on purpose.
Four Colour Additions, and One Rewrites Your Tokens
alpha() is the small one that will show up in every design system inside a month:
:root { --brand: oklch(55% 0.21 258); }
.overlay {
/* Before: restate every channel you are not changing. */
background: oklch(from var(--brand) l c h / 80%);
/* After. */
background: alpha(from var(--brand) / 80%);
}
The formal grammar is alpha( [from <color>] [/ [<alpha-value> | none]]? ), and the reason it matters isn't brevity. The full relative syntax makes you name a colour space and restate the channels you're leaving alone, and each of those is a chance to shift a hue by accident. alpha() keeps the origin colour in its own space and changes exactly one channel. A token file that needs a 12%, a 40% and an 80% variant of nine brand colours goes from a wall of conversions to twenty-seven honest lines.
color-mix() now takes more than two colours, which is why the middle swatch in the preview above has three. That lands differently than it reads: a two-colour mix is a blend, a three-colour mix with explicit percentages is a small palette definition, and you can now write a tertiary directly rather than nesting mixes inside each other.
The other two are narrower. image(<color>) lets a solid colour stand where an image is expected, and light-dark() now accepts image values, so a scheme-aware background image stops needing a media query:
.panel {
background-image: light-dark(url("/paper-light.avif"), url("/paper-dark.avif"));
}
If you're composing tokens rather than reading about them, the palette generator below produces the OKLCH ramp these functions expect, and the swatches it gives you paste straight into an alpha() origin.
progress() Grows an Escape Hatch
progress(value, start, end) returns how far a value sits between two bounds, clamped to 0 through 1. no-clamp removes the clamp:
/* Clamps at 1, so the effect flattens past the boundary. */
--lift: progress(300, 0, 100);
/* Returns 3, so the effect keeps going. */
--lift: progress(no-clamp 300, 0, 100);
The keyword goes first and takes no comma after it. Why care? Because clamping is what makes scroll-driven motion feel like it hits a wall. An overscroll bounce, a parallax layer that should keep drifting past a section, a shadow that should keep growing while you pull: all of those want extrapolation, and all of them were previously faked with a second calc() that undid the clamp.
My own rule after a day with it: unclamp the source and clamp the consumer. An unbounded custom property will eventually meet an input nobody sanity-checked, and a translate of 4000px is harder to debug than a flat animation.
:host:has() Closes a Component Hole
:host:has() lets a web component style itself from the state of its own children:
:host:has(input:invalid) {
border-color: oklch(58% 0.18 22);
}
Before this, a component that wanted to react to its own content either exported a part and asked the page to style it, or set an attribute from JavaScript on every state change. Neither is wrong. Both mean the component's appearance lives somewhere other than the component.
The attribute-selector change belongs beside it. Attribute matching can now be made explicitly case-sensitive with an s modifier, the mirror of the i modifier we've had for years, which matters for anything keyed on a value where case carries meaning.
Three Smaller Ones, Because Somebody Will Hit Them
contain: style now applies to CSS quotes. That reads like nothing until you have a component that sets quotes and gets nested inside another that also sets quotes: the counter for nesting depth is document-wide, so the inner component picks up the outer one's level and shows the wrong glyph. Style containment scopes that counter to the container. It's a bug fix for a problem most people worked around by not using quotes at all.
roundRect() on a canvas context now takes its radii argument optionally, so ctx.roundRect(x, y, w, h) draws a plain rectangle instead of throwing. Small, but it removes an if from every wrapper anybody wrote around it.
And SVG <use> can reference an external file without a fragment identifier. <use href="icon.svg" /> works where you previously had to write icon.svg#root and therefore had to put an id on the root of every icon you shipped. If you generate a sprite sheet, nothing changes. If you ship one file per icon, you just deleted a build step.
What I'd Adopt Today, and What I'd Wait On
Shipping order matters more than the feature list, because Safari is one engine and none of this is interoperable yet. My split after a week:
| Feature | Ship now? | How |
|---|---|---|
overflow-anchor | Yes, by doing nothing | It's a default; the only work is none where you manage scroll yourself |
alpha() | Yes, with a fallback | Declare the opaque colour first, then the alpha() line |
color-mix() with 3+ colours | Yes, same pattern | A browser that parses only two colours drops the declaration |
light-dark() with images | Yes | The media-query version is a two-line fallback, not a rewrite |
progress() with no-clamp | Careful | The unclamped value is unbounded, so clamp where you consume it |
:host:has() | Yes inside a component | Shadow DOM already forces you to fall back gracefully |
| Safari MCP | No | Single vendor, unproven protocol |
The pattern for every "yes with a fallback" row is the same two lines, and it's the pattern the preview at the top of this page is built on:
.chip {
background: #2563eb;
background: alpha(from #2563eb / 80%);
}
An older engine parses line one, fails to parse line two, and keeps line one. That's the cascade doing exactly what it was designed for, and it's why these four colour functions are safe to use in production this week rather than next year. Is a translucent chip worth two declarations? For a design system with nine brand colours, yes, easily.
The Six From June, Re-checked
All six shipped, and one gained a detail worth knowing.
appearance: base-select with ::picker-icon, ::checkmark and <selectedcontent> is in. Anchor positioning follows transform, translate, rotate and scale, and the rename to anchor-valid and anchor-visible is final, with position-anchor now defaulting to normal. :heading is in, still carrying the specificity trap I flagged: it's a pseudo-class, so swapping your six-element reset for it strengthens the rule as well as shortening it. revert-rule, stretch and the subpixel inline layout work all landed as described.
So the June advice stands. If you planned your Safari 27 work off the beta post, you planned it correctly, which is the useful thing to be able to say about a beta.
The Number Nobody Quotes
844 resolved issues, against 525 announced at WWDC. That means roughly 319 fixes landed in the beta window alone, and 66 of them are in SVG.
For interface work a release's fix count tells you more about your next six months than its feature count does, because features you adopt when you choose to and fixes decide whether the thing you already shipped behaves the same in two browsers.
I find that number more persuasive than the feature list, and here's the opinion: for interface work, a release's fix count tells you more about your next six months than its feature count does. Features you adopt when you choose to. Fixes decide whether the thing you already shipped behaves the same in two browsers. A 60% increase in resolved issues between WWDC and release is a quality release wearing a feature release's headline.
Safari MCP, Briefly
Safari 27 ships an MCP server so coding agents can drive the browser. I'm noting it rather than recommending it, for the same reason I said to prototype rather than ship the <model> element: it's single-vendor, and the interesting question is whether the protocol survives contact with a second engine. Worth an afternoon. Not worth a workflow yet.
Summary
Safari 27.0 landed on September 17, 2026 with 83 features, 25 more than the June beta, and 844 resolved issues against the 525 announced at WWDC. For interface work the delta comes down to four things: overflow-anchor brings scroll anchoring on by default, so content loading above the viewport stops stealing the reader's place; alpha(from var(--brand) / 80%) collapses the relative-colour boilerplate that token systems were drowning in, alongside multi-colour color-mix(), image(<color>) and light-dark() accepting images; progress() gains no-clamp for motion that should extrapolate past its bounds rather than flatten; and :host:has() finally lets a web component style itself from its own children's state. Everything I flagged in the beta shipped unchanged, so the six from June remain the six.
Try our own free generator
Enough theory. We built our own color palette generator and use it on real client work, so go ahead and test it right here. Pick four colors, watch them land on a working interface, and read the live WCAG contrast before you commit to anything. Nothing leaves your browser.