Skip to content

What Arrived in Safari 27 Between June and September

Safari 27 stable shipped 83 features against the beta's 58. Scroll anchoring, four colour functions and an unclamped progress() reach your CSS.

· · 10 min read
A circle of paper colour swatch cards arranged on a table

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 including alpha(), an unclamped progress(), 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.

AreaNew since June
CSS colouralpha(), multi-colour color-mix(), image(<color>), light-dark() taking images
Scrollingoverflow-anchor, defaulting to auto
CSS valuesprogress() gains no-clamp
Selectors:host:has(), case-sensitive s on attribute selectors
Containmentcontain: style for CSS quotes
Canvas and SVGoptional radii on roundRect(), <use> referencing external files without a fragment
ToolingSafari 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.

Photo by Jonas Leupe on Unsplash

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

Photo by Christina Rumpf on Unsplash

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:

FeatureShip now?How
overflow-anchorYes, by doing nothingIt's a default; the only work is none where you manage scroll yourself
alpha()Yes, with a fallbackDeclare the opaque colour first, then the alpha() line
color-mix() with 3+ coloursYes, same patternA browser that parses only two colours drops the declaration
light-dark() with imagesYesThe media-query version is a two-line fallback, not a rewrite
progress() with no-clampCarefulThe unclamped value is unbounded, so clamp where you consume it
:host:has()Yes inside a componentShadow DOM already forces you to fall back gracefully
Safari MCPNoSingle 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.

The three colour functions that are new in 27.0
Edit the CSS

Change 80% to 20% in the alpha() row. If that swatch does not move, your browser is older than Safari 27 and you are looking at the fallback declared on the line above it. Then add a fourth colour to the color-mix() row and watch the weights redistribute.

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.

Try it: build a palette here

Pick four colors, watch them land on a real interface, and check the contrast before you ship. Hit Space to shuffle the unlocked swatches. Everything runs in your browser.

Background #ffffff
Text #0f172a
Primary #2563eb
Accent #ea4335
New

Design that reads at a glance

A quiet layout, one confident accent, and type that stays out of its own way. This is your palette on a working screen, not a swatch board.

Fast iteration

Lock what works. Shuffle the rest until it clicks.

In context

Judge color where it lives, on buttons and cards.

This is the embedded version. For the full-screen free UI color palette generator, with shareable palette links, live WCAG contrast checks and CSS variable export, open the standalone tool page.

Frequently Asked Questions

How many features are actually new since the Safari 27 beta?
Twenty-five. WebKit's own release 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. The 25 are not evenly interesting: scroll anchoring through overflow-anchor, four additions to the colour functions, an unclamped progress() and the :host:has() compound selector are the ones that reach a stylesheet. The rest is engine, spatial and WKWebView work that a web developer will never type.
Do I need to change anything because of scroll anchoring?
Probably not, and that is the point of how it shipped. overflow-anchor defaults to auto, so the browser now holds your scroll position when content loads above the viewport, which is the behaviour you always wanted. You only reach for it when the anchoring fights something you built yourself, most often a virtualised list or a carousel that manages scrollTop by hand. There the fix is overflow-anchor: none on the scroll container, scoped as narrowly as you can, because turning it off globally hands the old content-jump problem back to your readers.
Is alpha() just shorter relative colour syntax?
Shorter and less error-prone. The full relative syntax makes you restate the channels you are not changing, as in oklch(from var(--brand) l c h / 80%), and every one of those channel keywords is a chance to pick the wrong colour space and shift the hue by accident. alpha(from var(--brand) / 80%) keeps the origin colour in its own space and touches only the alpha channel. For a token system that needs a translucent variant of every brand colour, that is the difference between one legible line and a table of hand-written conversions.
What does no-clamp on progress() actually unlock?
Extrapolation past the bounds you declared. progress(300, 0, 100) clamps to 1, so an effect driven by it flattens the moment the input passes the end of its range. progress(no-clamp 300, 0, 100) returns 3, so the effect keeps scaling. That is what you want for overscroll behaviour, for a parallax layer that should keep moving past a section boundary, and for any motion where hitting the ceiling should look like momentum rather than a wall. It is also a good way to ship a value nobody sanity-checked, so clamp the consumer even when you unclamp the source.