It’s been possible to create gorgeous layouts on the web for decades, but often refining the details to perfection can take a lot of effort. Arguably too much effort, and developers just don’t have the time. Happily, the last few years of CSS have seen a lot of new tools, sometimes very simple tools, that when used in a smart fashion suddenly make polishing graphic design details incredibly easy. One such tool is the lh unit.
Line height units
It might seem like no big deal that the web gained two new units, lh and rlh, to reference line heights. Support shipped in every browser in 2023 without much fanfare. But wow, are they powerful.
Basically, 1lh equals the height of one line of text, for the current font at the current line height. “LH” stands for Line Height. The accompanying 1rlh unit is the equivalent of one line height at the root, just like how rem is the em at the root. “RLH” stands for Root Line Height.
My favorite thing to do with the lh unit is to set margins on content. Let’s set a new universal margin on paragraphs with:
p { margin-block: 1lh; }
You can see the results in the following screenshots. On the left, the margin in the block direction is set to 1em, the default in UA stylesheets since the 90s. On the right it’s changed to 1lh.
Many people who have an eye for layout and spacing can immediately see the difference. You might agree that the version on the right just looks more polished. It looks refined. While the version on the left looks a bit clunky. It looks, well, like everything on the web has looked for decades. Slightly awkward.
Many other people will look at this comparison and think “I don’t see it” or “what’s the big deal”? Let’s draw a line grid over the text to make the difference more clear. Hopefully now it’s more obvious that the blank space between paragraphs is equivalent to a line of text when it’s defined in lh units.
Line height units give us a direct way to tie any size in our layout to the vertical rhythm of the text. Margins are just one possibility — padding is another, gaps, width and height, or any other measurement in the layout.
Try it yourself
You can try out combinations of options to see the effects of using lh for paragraph margins in this demo.
Browser support
Line height units are supported in over 94% of the browsers people use today. For that last 6%, you can use progressive enhancement to ensure they get a good experience. For example:
article {
padding: 1em; /* fallback for browsers lh without support */padding: 1lh;
}
Using this technique causes browsers without support to render 1em of padding, while the browsers with support render 1lh of padding.
Take your typography to the next level
It really is a great time for typography on the web. There’re over a dozen small features that shipped in the last few years that empower web designers & developers to polish typographic details to a far better result, especially with the kind of easy robustness that makes it practical to accomplish. It’s my hope you use them!
Let me know what you think on Bluesky or Mastodon. I’d love to hear your stories, plans and questions.
In my previous post, when I introduced the switch to Skia for 2D rendering, I explained that we replaced Cairo with Skia keeping mostly the same architecture. This alone was an important improvement in performance, but still the graphics implementation was designed for Cairo and CPU rendering. Once we considered the switch to Skia as stable, we started to work on changes to take more advantage of Skia and GPU rendering to improve the performance even more. In this post I’m going to present some of those improvements and other not directly related to Skia and GPU rendering.
Explicit fence support
This is related to the DMA-BUF renderer used by the GTK port and WPE when using the new API. The composited buffer is shared as a DMA-BUF between the web and UI processes. Once the web process finished the composition we created a fence and waited for it, to make sure that when the UI process was notified that the composition was done the buffer was actually ready. This approach was safe, but slow. In 281640@main we introduced support for explicit fencing to the WPE port. When possible, an exportable fence is created, so that instead of waiting for it immediately, we export it as a file descriptor that is sent to the UI process as part of the message that notifies that a new frame has been composited. This unblocks the web process as soon as composition is done. When supported by the platform, for example in WPE under Wayland when the zwp_linux_explicit_synchronization_v1 protocol is available, the fence file descriptor is passed to the platform implementation. Otherwise, the UI process asynchronously waits for the fence by polling the file descriptor before passing the buffer to the platform. This is what we always do in the GTK port since 281744@main. This change improved the score of all MotionMark tests, see for example multiply.
Enable MSAA when available
In 282223@main we enabled the support for MSAA when possible in the WPE port only, because this is more important for embedded devices where we use 4 samples providing good enough quality with a better performance. This change improved the Motion Mark tests that use 2D canvas like canvas arcs, paths and canvas lines. You can see here the change in paths when run in a RaspberryPi 4 with WPE 64 bits.
Avoid textures copies in accelerated 2D canvas
As I also explained in the previous post, when 2D canvas is accelerated we now use a dedicated layer that renders into a texture that is copied to be passed to the compositor. In 283460@main we changed the implementation to use a CoordinatedPlatformLayerBufferNativeImage to handle the canvas texture and avoid the copy, directly passing the texture to the compositor. This improved the MotionMark tests that use 2D canvas. See canvas arcs, for example.
Introduce threaded GPU painting mode
In the initial implementation of the GPU rendering mode, layers were painted in the main thread. In 287060@main we moved the rendering task to a dedicated thread when using the GPU, with the same threaded rendering architecture we have always used for CPU rendering, but limited to 1 worker thread. This improved the performance of several MotionMark tests like images, suits and multiply. See images.
Update default GPU thread settings
Parallelization is not so important for GPU rendering compared to CPU, but still we realized that we got better results by increasing a bit the amount of worker threads when doing GPU rendering. In 290781@main we increased the limit of GPU worker threads to 2 for systems with at least 4 CPU cores. This improved mainly images and suits in MotionMark. See suits.
Hybrid threaded CPU+GPU rendering mode
We had either GPU or CPU worker threads for layer rendering. In systems with 4 CPU cores or more we now have 2 GPU worker threads. When those 2 threads are busy rendering, why not using the CPU to render other pending tiles? And the same applies when doing CPU rendering, when all workers are busy, could we use the GPU to render other pending tasks? We tried and turned out to be a good idea, especially in embedded devices. In 291106@main we introduced the hybrid mode, giving priority to GPU or CPU workers depending on the default rendering mode, and also taking into account special cases like on HiDPI, where we are always scaling, and we always prefer the GPU. This improved multiply, images and suits. See images.
Use Skia API for display list implementation
When rendering with Cairo and threaded rendering enabled we use our own implementation of display lists specific to Cairo. When switching to Skia we thought it was a good idea to use the WebCore display list implementation instead, since it’s cross-platform implementation shared with other ports. But we realized this implementation is not yet ready to support multiple threads, because it holds references to WebCore objects that are not thread safe. Main thread might change those objects before they have been processed by painting threads. So, we decided to try to use the Skia API (SkPicture) that supports recording in the main thread and replaying from worker threads. In 292639@main we replaced the WebCore display list usage by SkPicture. This was expected to be a neutral change in terms of performance but it surprisingly improved several MotionMark tests like leaves, multiply and suits. See leaves.
Use Damage to track the dirty region of GraphicsLayer
Every time there’s a change in a GraphicsLayer and it needs to be repainted, it’s notified and the area that changed is included so that we only render the parts of the layer that changed. That’s what we call the layer dirty region. It can happen that when there are many small updates in a layer we end up with lots of dirty regions on every layer flush. We used to have a limit of 32 dirty regions per layer, so that when more than 32 are added we just united them into the first dirty area. This limit was removed because we always unite the dirty areas for the same tiles when processing the updates to prepare the rendering tasks. However, we also tried to avoid handling the same dirty region twice, so every time a new dirty region was added we iterated the existing regions to check if it was already present. Without the 32 regions limit that means we ended up iterating a potentially very long list on every dirty region addition. The damage propagation feature uses a Damage class to efficiently handle dirty regions, so we thought we could reuse it to track the layer dirty region, bringing back the limit but uniting in a more efficient way than using always the first dirty area of the list. It also allowed to remove check for duplicated area in the list. This change was added in 292747@main and improved the performance of MotionMark leaves and multiply tests. See leaves.
Record all dirty tiles of a layer once
After the switch to use SkPicture for the display list implementation, we realized that this API would also allow to record the graphics layer once, using the bounding box of the dirty region, and then replay multiple times on worker threads for every dirty tile. Recording can be a very heavy operation, specially when there are shadows or filters, and it was always done for every tile due to the limitations of the previous display list implementation. In 292929@main we introduced the change with improvements in MotionMark leaves and multiply tests. See multiply.
MotionMark results
I’ve shown here the improvements of these changes in some of the MotionMark tests. I have to say that some of those changes also introduced small regressions in other tests, but the global improvement is still noticeable. Here is a table with the scores of all tests before these improvements and current main branch run by WPE MiniBrowser in a RaspberryPi 4 (64bit).
Test
Score July 2024
Score April 2025
Multiply
501.17
684.23
Canvas arcs
140.24
828.05
Canvas lines
1613.93
3086.60
Paths
375.52
4255.65
Leaves
319.31
470.78
Images
162.69
267.78
Suits
232.91
445.80
Design
33.79
64.06
What’s next?
There’s still quite a lot of room for improvement, so we are already working on other features and exploring ideas to continue improving the performance. Some of those are:
Damage tracking: this feature is already present, but disabled by default because it’s still work in progress. We currently use the damage information to only paint the areas of every layer that changed. But then we always compose a whole frame inside WebKit that is passed to the UI process to be presented on screen. It’s possible to use the damage information to improve both, the composition inside WebKit and the presentation of the composited frame on the screen. For more details about this feature read Pawel’s awesome blog post about it.
Use DMA-BUF for tile textures to improve pixel transfer operations: We currently use DMA-BUF buffers to share the composited frame between the web and UI process. We are now exploring the idea of using DMA-BUF also for the textures used by the WebKit compositor to generate the frame. This would allow to improve the performance of pixel transfer operations, for example when doing CPU rendering we need to upload the dirty regions from main memory to a compositor texture on every composition. With DMA-BUF backed textures we can map the buffer into main memory and paint with the CPU directly into the mapped buffer.
Compositor synchronization: We plan to try to improve the synchronization of the WebKit compositor with the system vblank and the different sources of composition (painted layers, video layers, CSS animations, WebGL, etc.)
Safari Technology Preview Release 217 is now available for download for macOS Sequoia and macOS Sonoma. If you already have Safari Technology Preview installed, you can update it in System Settings under General → Software Update.
Fixed not marking content-visibility: hidden content for layout when targeting content-visibility: auto. (293304@main) (148663896)
SVG
Resolved Issues
Fixed: Improved handling of SVG images with subresources. (293259@main) (148607855)
Tables
Resolved Issues
Fixed table layout to only be triggered when inline-size is not auto. (292536@main) (147636653)
Web API
New Features
Added support for scrollMargin in IntersectionObserver. (293306@main) (117527880)
Added support for the alg parameter when importing or exporting Edward’s-curve based JSON Web Keys in WebCrypto. (293089@main) (147323269)
Web Extensions
Resolved Issues
Fixed "excludeMatches" array in scripting.registerContentScripts() API getting ignored in Safari web extensions. (293106@main) (145489255) (FB16590857)
Web Inspector
New Features
Added support for exporting and importing data from worker targets in the Timelines tab. (292991@main) (145330533)
Added a a badge for <slot> to quickly jump to the assigned node in the Elements tab. (292966@main) (148297936)
Resolved Issues
Fixed a performance issue when blackboxing a large number of sourcemaps. (292997@main) (148116377)
WebRTC
New Features
Added support for exposing CSRC information for RTCEncodedVideoStream. (293088@main) (76548862)
Added serialisation of RTCEncodedAudioFrame and RTCEncodedVideoFrame. (293185@main) (148244334)
Added support for ImageCapture.grabFrame. (293243@main) (148425176)
Resolved Issues
Fixed enumerateDevices returning devices as available when permissions are denied. (292921@main) (147313922)
Fixed getUserMedia not using the correct camera or microphone if devices change while user is answering the permission prompt. (292696@main) (147762070)
FixedenumerateDevices to not check for device permission. (293003@main) (148094614)
Fixed WebRTC encoded transform to transfer to the RTC encoded frame array buffer. (293232@main) (148343876)
Fixed RTC encoded frame timestamp should be persistent. (293238@main) (148580865)
Damage propagation is an optional WPE/GTK WebKit feature that — when enabled — reduces browser’s GPU utilization at the expense of increased CPU and memory utilization. It’s very useful especially in the context of low- and mid-end
embedded devices, where GPUs are most often not too powerful and thus become a performance bottleneck in many applications.
In computer graphics, the damage term is usually used in the context of repeatable rendering and means essentially “the region of a rendered scene that changed and requires repainting”.
In the context of WebKit, the above definition may be specialized a bit as WebKit’s rendering engine is about rendering web content to frames (passed further to the platform) in response to changes within a web page.
Thus the definition of WebKit’s damage refers, more specifically, to “the region of web page view that changed since previous frame and requires repainting”.
On the implementation level, the damage is almost always a collection of rectangles that cover the changed region. This is exactly the case for WPE and GTK WebKit ports.
To better understand what the above means, it’s recommended to carefully examine the below screenshot of GTK MiniBrowser as it depicts the rendering of the poster circle demo
with the damage visualizer activated:
In the image above, one can see the following elements:
the web page view — marked with a rectangle stroked to magenta color,
the damage — marked with red rectangles,
the browser elements — everything that lays above the rectangle stroked to a magenta color.
What the above image depicts in practice, is that during that particular frame rendering, the area highlighted red (the damage) has changed and needs to be repainted. Thus — as expected — only the moving parts of the demo require repainting.
It’s also worth emphasizing that in that case, it’s also easy to see how small fraction of the web page view requires repainting. Hence one can imagine the gains from the reduced amount of painting.
Normally, the job of the rendering engine is to paint the contents of a web page view to a frame (or buffer in more general terms) and provide such rendering result to the platform on every scene rendering iteration —
which usually is 60 times per second.
Without the damage propagation feature, the whole frame is marked as changed (the whole web page view) always. Therefore, the platform has to perform the full update of the pixels it has 60 times per second.
While in most of the use cases, the above approach is good enough, in the case of embedded devices with less powerful GPUs, this can be optimized. The basic idea is to produce the frame along with the damage information i.e. a hint for
the platform on what changed within the produced frame. With the damage provided (usually as an array of rectangles), the platform can optimize a lot of its operations as — effectively — it can
perform just a partial update of its internal memory. In practice, this usually means that fewer pixels require updating on the screen.
For the above optimization to work, the damage has to be calculated by the rendering engine for each frame and then propagated along with the produced frame up to its final destination. Thus the damage propagation can be summarized
as continuous damage calculation and propagation throughout the web engine.
Once the general idea has been highlighted, it’s possible to examine the damage propagation in more detail. Before reading further, however, it’s highly recommended for the reader to go carefully through the
famous “WPE Graphics architecture” article that gives a good overview of the WebKit graphics pipeline in general and which introduces the basic terminology
used in that context.
The information on the visual changes within the web page view has to travel a very long way before it reaches the final destination. As it traverses the thread and process boundaries in an orderly manner, it can be summarized
as forming a pipeline within the broader graphics pipeline. The image below presents an overview of such damage propagation pipeline:
This pipeline starts with the changes to the web page view visual state (RenderTree) being triggered by one of many possible sources. Such sources may include:
User interactions — e.g. moving mouse cursor around (and hence hovering elements etc.), typing text using keyboard etc.
Web API usage — e.g. the web page changing DOM, CSS etc.
multimedia — e.g. the media player in a playing state,
and many others.
Once the changes are induced for certain RenderObjects, their visual impact is calculated and encoded as rectangles called dirty as they
require re-painting within a GraphicsLayer the particular RenderObject
maps to. At this point, the visual changes may simply be called layer damage as the dirty rectangles are stored in the layer coordinate space and as they describe what changed within that certain layer since the last frame was rendered.
The next step in the pipeline is passing the layer damage of each GraphicsLayer
(GraphicsLayerCoordinated) to the WebKit’s compositor. This is done along with any other layer
updates and is mostly covered by the CoordinatedPlatformLayer.
The “coordinated” prefix of that name is not without meaning. As threaded accelerated compositing is usually used nowadays, passing the layer damage to the WebKit’s compositor must be coordinated between the main thread and
the compositor thread.
When the layer damage of each layer is passed to the WebKit’s compositor, it’s stored in the TextureMapperLayer that corresponds to the given
layer’s CoordinatedPlatformLayer. With that — and with all other layer-level updates — the
WebKit’s compositor can start computing the frame damage i.e. damage that is the final damage to be passed to the very end of the pipeline.
The first step to building frame damage is to process the layer updates. Layer updates describe changes of various layer properties such as size, position, transform, opacity, background color, etc. Many of those updates
have a visual impact on the final frame, therefore a portion of frame damage must be inferred from those changes. For example, a layer’s transform change that effectively changes the layer position means that the layer
visually disappears from one place and appears in the other. Thus the frame damage has to account for both the layer’s old and new position.
Once the layer updates are processed, WebKit’s compositor has a full set of information to take the layer damage of each layer into account. Thus in the second step, WebKit’s compositor traverses the tree formed out of
TextureMapperLayer objects and collects their layer damages. Once the layer damage of a certain layer
is collected, it’s transformed from the layer coordinate space into a global coordinate space so that it can be added to the frame damage directly.
After those two steps, the frame damage is ready. At this point, it can be used for a couple of extra use cases:
for WebKit’s compositor itself to perform some extra optimizations — as will be explained in the WebKit’s compositor optimizations section,
for layout tests.
Eventually — regardless of extra uses — the WebKit’s compositor composes the frame and sends it (a handle to it) to the UI Process along with frame damage using the IPC mechanism.
In the UI process, there are basically two options determining frame damage destiny — it can be either consumed or ignored — depending on the platform-facing implementation. At the moment of writing:
At the moment of writing, the damage propagation feature is run-time-disabled by default (PropagateDamagingInformation feature flag) and compile-time enabled by default for GTK and WPE (with new platform API) ports.
Overall, the feature works pretty well in the majority of real-world scenarios. However, there are still some uncovered code paths that lead to visual glitches. Therefore it’s fair to say the feature is still a work in progress.
The work, however, is pretty advanced. Moreover, the feature is set to a testable state and thus it’s active throughout all the layout test runs on CI.
Not only the feature is tested by every layout test that tests any kind of rendering, but it also has quite a lot of dedicated layout tests.
Not to mention the unit tests covering the Damage class.
In terms of functionalities, when the feature is enabled it:
activates the damage propagation pipeline and hence propagates the damage up to the platform,
When the feature is enabled, the main goal is to activate the damage propagation pipeline so that eventually the damage can be provided to the platform. However, in reality, a substantial part of the pipeline is always active
regardless of the features being enabled or compiled. This part of the pipeline ends before the damage reaches
CoordinatedPlatformLayer and is always active because it was used for layer-level optimizations for a long time.
More specifically — this part of the pipeline existed long before the damage propagation feature and was using layer damage to optimize the layer painting to the intermediate surfaces.
Because of the above, when the feature is enabled, only the part of the pipeline that starts with CoordinatedPlatformLayer
is activated. It is, however, still a significant portion of the pipeline and therefore it implies additional CPU/memory costs.
When the feature is activated and the damage flows through the WebKit’s compositor, it creates a unique opportunity for the compositor to utilize that information and reduce the amount of painting/compositing it has to perform.
At the moment of writing, the GTK/WPE WebKit’s compositor is using the damage to optimize the following:
to apply global glScissor to define the smallest possible clipping rect for all the painting it does — thus reducing the amount of painting,
to reduce the amount of painting when compositing the tiles of the layers using tiled backing stores.
Detailed descriptions of the above optimizations are well beyond the scope of this article and thus will be provided in one of the next articles on the subject of damage propagation.
As mentioned in the above sections, the feature only works in the GTK and the new-platform-API-powered WPE ports. This means that:
In the case of GTK, one can use MiniBrowser or any up-to-date GTK-WebKit-derived browser to test the feature.
In the case of WPE with the new WPE platform API the cog browser cannot be used as it uses the old API. Therefore, one has to use MiniBrowser
with the --use-wpe-platform-api argument to activate the new WPE platform API.
Moreover, as the feature is run-time-disabled by default, it’s necessary to activate it. In the case of MiniBrowser, the switch is --features=+PropagateDamagingInformation.
For quick testing, it’s highly recommended to use the latest revision of WebKit@main with wkdev SDK container and with GTK port.
Assuming one has set up the container, the commands to build and run GTK’s MiniBrowser are as follows:
It’s also worth mentioning that WEBKIT_SHOW_DAMAGE=1 environment variable disables damage-driven GTK/WPE WebKit’s compositor optimizations and therefore some glitches that are seen without the envvar, may not be seen
when it is set. The URL to this presentation is a great example to explore various glitches that are yet to be fixed. To trigger them, it’s enough to navigate
around the presentation using top/right/down/left arrows.
This article was meant to scratch the surface of the broad, damage propagation topic. While it focused mostly on introducing basic terminology and describing the damage propagation pipeline in more detail,
it briefly mentioned or skipped completely the following aspects of the feature:
the problem of storing the damage information efficiently,
the damage-driven optimizations of the GTK/WPE WebKit’s compositor,
the most common use cases for the feature,
the benchmark results on desktop-class and embedded devices.
Therefore, in the next articles, the above topics will be examined to a larger extent.
The new WPE platform API is still not released and thus it’s not yet officially announced. Some information on it, however, is provided by
this presentation prepared for a WebKit contributors meeting.
The platform that the WebKit renders to depends on the WebKit port:
in case of GTK port, the platform is GTK so the rendering is done to GtkWidget,
in case of WPE port with new WPE platform API, the platform is one of the following:
wayland — in that case rendering is done to the system’s compositor,
DRM — in that case rendering is done directly to the screen,
headless — in that case rendering is usually done into memory buffer.
Shapes are an important aspect of graphic design. The shape() function provides a new way to author shapes that can adapt and scale with mixed proportions based on the size of the element.
For many years now, CSS has given web developers the ability to use shapes for clipping and animation. The most common usage of shapes is in theclip-path property. This applies a clip after the element has been laid out, which allows you, as a web developer, to do things like trim the edge to a decorative wavy shape. This can give a similar effect to masking, but is more efficient to render than a mask. Another use case for shapes is in theoffset-path property, which allows you to animate something along a path. Finally,shape-outside provides a way to wrap text around a non-rectangular shape (but currently only supports a subset of the shape types).
CSS provides a set of shapes that can be used with each of these properties. (In the web standards, these are fully defined in Basic Shapes.) There are various ways to describe rectangles, as well as circle() and ellipse(), polygon() (a set of straight-line segments) and the path() function, which takes an SVG-style path.
For example, consider this simple arrow shape:
The SVG that produces this shape is:
<svgviewBox="0 0 150 100"xmlns="http://www.w3.org/2000/svg"><pathfill="black"d="M0 0 L 100 0 L 150 50 L 100 100 L 0 100 Q 50 50 0 0 z " /></svg>
We’ll break down this path later. For now, let’s take that path and apply it to an HTML element in clip-path:
.clipped {
width: 150px;
height: 100px;
box-sizing: border-box;
background-color: blue;
border: 10pxsolidorange;
clip-path: path("M0 0 L 100 0 L 150 50 L 100 100 L 0 100 Q 50 50 0 0 z");
}
Which produces this:
Note how I’ve added a border so that you can see how clip-path is clipping out parts of the element.
But what happens now if we change the dimensions of the element? For example, what if we wanted a longer arrow?
.clipped {
width: 200px;
...
}
Alas, we get the same shape, only we no longer see the border at the tip of the arrow.
This means that using path() in clip-path can’t be responsive; you can’t write CSS rules so that the path adapts to the size of the element. This is where the new shape() function comes in.
shape()
The new shape() function addresses this responsiveness problem head-on by allowing you to specify the path in terms of CSS keywords and units that you’re already familiar with, including the full power of calc() and CSS variables, and it’s much more readable.
Let’s go back and break down that SVG path which looks like an incomprehensible list of letters and numbers: “M0 0 L 100 0 L 150 50 L 100 100 L 0 100 Q 50 50 0 0 z”. We can break this down into a sequence of “commands”, which describe each section of the path:
M 0 0
move to 0, 0
L 100 0
line to 100, 0
L 150 50
line to 150, 50
L 100 100
line to 100, 100
L 0 100
line to 0, 100
Q 50 50 0 0
quadratic curve with control point at 50, 50, ending at 0, 0
z
close path
We can transcribe this path into a shape using the names specified in the CSS spec; note how the first “move” command becomes the “from” in the shape:
Note that we can use keywords like “top” and “bottom left” when the point is in absolute coordinates, and we can use percentages.
That gives us the same result as above, but we haven’t made it fully responsive yet. Our next step is to make the shape stretch horizontally with the element’s width, but keep the height fixed to 100px. Let’s use a bit of math to achieve that. To simplify things a little, lets put the desired height of the element into a variable:
We’ll also define the half-height value to compute the arrow shape on the right side, and for the control point of the quadratic curve.
--half-height: calc(var(--height) / 2);
The responsive parts of the shape can be written in terms of percentages. Percentages used in x coordinates are relative to the element’s width, while the ones used in y coordinates are relative to the element’s height:
Now we have a clip that can be as long as the element!
Making this shape adapt to the height of the element is trickier, because we need to be able to refer to the height in values on the horizontal axis. This is something we can do using Container Queries. First, let’s create a container in the HTML markup:
Resizing the arrow helps to truly demonstrate what shape() can do. Notice how the curve and the 90 degree angle are preserved, which is not something you would get if you stretched an SVG.
There are more features of the shape() function that we haven’t touched on here, like relative instead of absolute commands (e.g. move by 10px, 2em), other commands like arc, and various ways of describing curve control points. Dive into the web standard to learn more about these. And, of course, you can animate between shapes, as long as they contain the same list of commands, which can make for some very cool effects.
GStreamer-based multimedia support for WebKit, including (but not limited to) playback, capture, WebAudio, WebCodecs, and WebRTC.
On the WebRTC front, basic support for Rapid Synchronization was added, along with a couple of spec coverage improvements (https://commits.webkit.org/293567@main, https://commits.webkit.org/293569@main).
Dispatch a "canceled" error event for all queued utterances in case of SpeechSynthesis.
Support for the Camera desktop portal was added recently, it will benefit mostly Flatpak apps using WebKitGTK, such as GNOME Web, for access to capture devices, which is a requirement for WebRTC support.
JavaScriptCore 🐟
The built-in JavaScript/ECMAScript engine for WebKit, also known as JSC or SquirrelFish.
Work continued on porting the in-place wasm interpreter (IPInt) to 32-bits.
We have been working on bringing the Temporal implementation in JSC up to the current spec, and a step towards that goal was implemented in WebKit PR #43849. This PR changes how calendar annotation key parsing works; it doesn't change anything observable, but sets the groundwork for parsing calendar critical flags and unknown annotations.
Releases 📦️
The recent releases of WebKitGTK and WPE WebKit 2.48 introduced a number of improvements to performance, reduced resource usage, better support for web platform features and standards, multimedia, and more!
Read more about these updates in the freshly published articles for WebKitGTK, and WPE WebKit.
Pawel Lampe published a blog post on the damage propagation feature. This feature reduces browser's GPU utilization at the expense of increased CPU and memory utilization in the WPE and GTK WebKit ports.
Our efforts to bring GstWebRTC support to WebKitGTK and WPEWebKit also include direct contributions to GStreamer. We recently improved WebRTC spec compliance in webrtcbin, by making the SDP mid attribute optional in offers and answers.
The WPE WebKit team has been working hard during the past six months and has recently released version 2.48 of the WPE port of WebKit. As is now tradition, here is an overview of the most important changes in this new stable release series.
Graphics and Rendering
The work on the graphics pipeline of WebKit continues, and a lot of improvements, refactorings, bug fixes, and optimizations have happened under the hood. These changes bring both performance and rendering improvements, and are too many to list individually. Nonetheless, there are a number of interesting changes.
GPU Worker Threads
When GPU rendering with Skia is in use, tiles will be rendered in worker threads, which has a positive impact on performance. Threads were already used when using the CPU for rendering. Note that GPU usage for rendering is not yet the default in the WPE port, but may be enabled setting WEBKIT_SKIA_ENABLE_CPU_RENDERING=0 in the environment before running programs that use WPE WebKit.
Canvas Improvements
The CanvasRenderingContext2DputImageData() and getImageData() methods have been optimized by preventing unnecessary buffer copies, resulting in improved performance and reduced memory usage.
CSS 3D Transforms
There have been several improvements handling elements that use preserve-3dCSS transforms. On top of a modest performance improvement, the changes fixed rendering issues, making the implementation compliant with the specification.
Damage Tracking
This release gained experimental support for collecting “damage” information, which tracks which parts of Web content produce visual changes in the displayed output. This information is then taken into account to reuse existing graphics buffers and repaint only those parts that need to be modified. This results better performance and less resource usage.
Note that this feature is disabled by default and may be previewed toggling the PropagateDamagingInformation feature flag.
GPU Process Beginnings
A new “GPU process” is now always built, but its usage is disabled by default at runtime. This is an experimental feature that can be toggled via the UseGPUProcessForWebGL feature flag, and as the name implies at the moment this new auxiliary process only supports handling WebGL content.
The GPU process is a new addition to WebKit’s multiprocess model, in which isolated processes are responsible for different tasks: the GPU process will eventually be in charge of most tasks that make use of the graphics processing unit, in order to improve security by separating graphics handling from Web content and data access. At the same time, graphics-intensive work does not interfere with Web content handling, which may bring potential performance improvements in the future.
Multimedia
The MediaRecorder backend gained support for the WebM format and audio bitrate configuration. WebM usage requires GStreamer 1.24.9 or newer.
Video handling using the WebCodecs API no longer ignores the prefer-hardware option. It is used as a hint to attempt using hardware-accelerated GStreamer components. If that fails, software based codecs will be used as fallback.
The Web Speech API gained a new synthesis backend, using libspiel. The existing FLite-based backend is still chosen by default because the dependency is readily available in most distributions, but setting USE_SPIEL=ON at build time is recommended where libspiel may be available.
The GStreamer-GL sink can now handle DMA-BUF memory buffers, replacing the DMA-BUF sink in this way.
The WPEPlatform library is a completely new API which changes how WPE embedding API works. The aim is to make both developing WPE backends and user applications more approachable and idiomatic using GLib and GObject conventions. Inspiration has been drawn from the Cog API. A preview version is shipped along in 2.48, and as such it needs to be explicitly enabled at build time with ENABLE_WPE_PLATFORM=ON. The API may still change and applications developed using WPEPlatform are likely to need changes with future WPE WebKit releases.
Web Platform
The list of Web Platform features that are newly available in 2.48 is considerably long, and includes the following highlights:
Support for CSS progress(), media-progress() and container-progress() functions—all part of the CSS, behind runtime flags CSSMediaProgressFunction and CSSContainerProgressFunction, respectively.
Packaging
The Web Inspector resources are no longer built into a shared library, but as a GResource bundle file. This avoids usage of the dynamic linker and allows mapping the file in memory, which brings improved resource usage.
Packagers and distributors might need some adaptation of their build infrastructure in cases where the Web Inspector resources have some ad-hoc handling. Typical cases are avoiding installation in production builds for embedded devices, or as an optional component for development builds.
For example, using the default build configuration, the file with the resources inside the installation prefix will be share/wpe-webkit-2.0/inspector.gresource, instead of the old lib/wpe-webkit-2.0/libWPEWebInspectorResources.so library.
Other Noteworthy Changes
The minimum required ICU version is now 70.1.
Reading of Remote Web Inspector protocol messages was optimized, resulting in a considerable speed-up for large messages.
The WPE WebKit team is already working on the 2.50 release, which is planned for September. In the meantime, you can expect stable updates for the 2.48 series through the usual channels.
Support for text-wrap: pretty just shipped in Safari Technology Preview, bringing an unprecedented level of polish to typography on the web. Let’s take a look at what the WebKit version of pretty does — it’s probably a lot more than you expect. Then, we’ll compare it to balance and the other text-wrap values to better understand when to use which one.
Ideas of what makes for “good” typography are deeply rooted in eras when type was set by hand using metal, wood, or ink. Typesetters took great care when deciding if a word should go on the end of one line, the beginning of the next, or be broken with a hyphen. Their efforts improved comprehension, reduced eye-strain, and simply made the reading experience more pleasant. While beauty can be subjective, with disagreements about what’s “better”, there are also strongly-held typographic traditions around the globe, representing various languages and scripts. These traditions carry people’s culture from one generation to the next, through the centuries.
In digital typography, a computer places all the words, not humans. Often, as a web designer or developer, you are creating a template to be filled with different versions of content. There is no “hand tweaking” typography on the web, especially when the layout is fluid, reflowing to fit different shapes and sizes of screens. So what can we do now to better express the expectations of quality from traditional typography, while still relying on the mechanization brought by today’s computers?
One solution is text-wrap:pretty. It’s intended to bring a new level of polish to type on the web by leveraging paragraph-based algorithms to solve long-standing problems.
Better typography
There are several things certain typographic traditions teach you to do:
Avoid short last lines. You want to avoid leaving a single word by itself on the end of a paragraph. It can look quite strange, and make space between paragraphs look mistakenly large.
Avoid “bad rag”. You can look at the ragged edge of the text (the inline-end side) and note whether the general length of lines is kind of consistent, or whether the rag is very jagged. When hand setting, typographers would move words around to minimize visual differences between adjacent lines — to avoid bag rag. “Good rag” increases readability of the text, and makes the entire block of text look more pleasing.
Avoid poor hyphenation. For languages that can be hyphenated, hyphenation helps create good rag. It also breaks a word into pieces, and places those pieces as far apart as possible in the inline dimension. This adds to the cognitive load when reading. It’s best to minimize the use of hyphenation and to avoid hyphenating two lines in a row.
Avoid typographic rivers. If you know to look for rivers, you might start to notice that sometimes the spaces between words randomly line up across lines, creating a path of whitespace through the paragraph. Rivers can be distracting.
You can see these four problems in the following example. The text is identical on both the left and the right.
Designers and typographers often use hyphenation and/or justification to help improve rag, but on the web, neither provides satisfying results. Until recently, it’s been impossible to do much of anything about short lines, bad rag, or rivers on the web. We’ve just lived with them.
Web line layout since 1991
For over 30 years, the web had only one technique for determining where to wrap text.
The browser starts with the first line of text, and lays out each word or syllable, one after another until it runs out of room. As soon as it has no more space to fit another word/syllable, it wraps to the next line (if wrapping is allowed). Then it starts on the next line, fitting all the content it can… then when it runs out of room, it wraps… and starts working on the next line.
It’s always thinking about only one line at a time. It wraps whenever it needs, after it’s fit the maximum amount of content on the previous line. If hyphenation is turned on, it will hyphenate whatever word is last on the line, at whatever point leaves as much of the word on the previous line as possible. Nothing else is taken into consideration — which is why text on the web has bad rag, rivers, short last lines, and hyphenation that makes no sense.
This is not required by the fact that text is laid out by a computer. For decades, software like Adobe InDesign and LaTeX has evaluated multiple lines of text at a time as they decide where to end one line and begin the next. It’s just that the web didn’t use a multiline algorithm. Until now.
We are excited to bring this capability to the web for the first time, in Safari Technology Preview 216.
text-wrap: pretty
Now, the web has the ability to evaluate the whole passage of text when figuring out where best to wrap. You can ask browsers to do this by using text-wrap: pretty. WebKit is not the first browser engine to implement, but we are the first browser to use it to evaluate and adjust the entire paragraph. And we are the first browser to use it to improve rag. We chose to take a more comprehensive approach in our implementation because we want you to be able to use this CSS to make your text easier to read and softer on the eyes, to provide your users with better readability and accessibility. And simply, to make something beautiful.
Safari Technology Preview 216 prevents short lines, improves the rag, and reduces the need for hyphenation — across all of the text, no matter how long it is. We are not yet making adjustments to prevent rivers, although we’d love to in the future.
While support for pretty shipped in Chrome 117, Edge 177, and Opera 103 in Fall 2023, and Samsung Internet 24 in 2024, the Chromium version is more limited in what it accomplishes. According to an article by the Chrome team, Chromium only makes adjustments to the last four lines of a paragraph. It’s focused on preventing short last lines. It also adjusts hyphenation if consecutive hyphenated lines appear at the end of a paragraph.
The purpose of pretty, as designed by the CSS Working Group, is for each browser to do what it can to improve how text wraps. The CSS Text Level 4 specification currently defines it like this, (where “user agent” means the web browser, emphasis added):
The user agent may among other things attempt to avoid excessively short last lines… but it should also improve the layout in additional ways. The precise set of improvements is user agent dependent, and may include things such as: reducing the variation in length between lines; avoiding typographic rivers; prioritizing different classes of soft wrap opportunities, hyphenation opportunities, or justification opportunities; avoiding hyphenation on too many consecutive lines.
The use of the word “may” is very meaningful here. It’s a clear statement that each browser gets to decide for itself exactly what pretty should do. There is no mandate for every browser to make the same choices. In fact, a browser team might decide in 2025 to handle some aspects of improving these qualities, and then change what their implementation does in the future.
Because of the way Chrome’s implementation of pretty has been taught, a lot of web developers expect this value is only supposed to prevent short last lines. But that was never the intention. In fact, the CSS Working Group defined a different value for such a purpose. It was just renamed last week to text-wrap: avoid-short-last-lines.
Take a look
You can try out text-wrap: pretty today in Safari Technology Preview 216. Check out this demo where you can toggle pretty on and off to see its effects. You can also toggle hyphenation or justification to try out any combination. Show guides and show ghosts will help you understand what’s happening. Or try out text-wrap: balance to see the difference. The demo has content in English, Arabic, German, Chinese and Japanese so you can see the effects in different writing systems.
Here’s a sample of text in English, without applying text-wrap. This is the default wrapping algorithm the web has had for years. I’ve turned on “show guides” to mark the edges of the text box. The green line marks the inline-end edge of the box — where the line layout algorithm is aiming for each line to reach. The browser wraps when the text reaches this green line.
And here’s how the same text looks instead with text-wrap: pretty applied. The green line has moved. Now, the browser instead aims to wrap each line sooner than the maximum limit of the text box. It wraps within the range, definitely after the magenta line, and definitely before the red line. This improves the rag.
You can also “show ghosts” to view the unprettified version as a ghost in the background.
You can also toggle hyphenation and justification on and off to compare different combinations. Resize the browser window, and see what happens at different widths of text.
You might notice that since Safari Technology Preview applies pretty to every line of text, not just the last four lines, it has a more significant impact on the text. The block of text becomes more of a solid rectangle.
You really have to typeset body text with text-wrap: pretty to see just how much of a difference it makes. It’s subtle, but remarkable. Combine this with paragraph margins of 1lh, and the text starts looking fantastic.
So why doesn’t every browser do everything it can to make the text look better? Because of performance.
Performance
Many developers are understandably concerned about the performance of text-wrap: pretty. While better typography is visually pleasing — it cannot come at the expense of a slower page. Each browser engineering team must think about the hardware and chips their users have when deciding how to set limits.
We are thrilled with the work we’ve done to ensure Safari users don’t experience a negative performance impact, even when web developers apply text-wrap: pretty to a lot of content on the web page.
One thing to know as a developer, the performance of text-wrap is not affected by how many elements on the page it’s applied to. Perf concerns emerge as the pretty algorithm takes more and more lines into consideration as it calculates what to do. In WebKit-based browsers or apps, your text element would need to be many hundreds or thousands of lines long to see a performance hit — and that kind of content is unusual on the web. If your content is broken up into typical-length paragraphs, then you have no reason to worry. Use text-wrap: pretty as much as you want, and rely on our browser engineers to ensure your users will not experience any downsides.
We might decide add a mechanism to take really long paragraphs and break them up into more reasonable chunks, where WebKit evaluates each chunk separately. If we do so, then even 1,000-line paragraphs won’t affect performance. That’s the approach Adobe InDesign takes. It improves the layout of all lines of text, but it doesn’t evaluate an infinite number of lines inside each paragraph all at once, in one pass. There also might be other ways the WebKit team discovers to balance beauty and speed, ensuring pretty does the most to improve all of your text without affecting the users experience of our incredibly fast browser.
Test out text-wrap: pretty in Safari Technology Preview 216 today, and let us know if you can trigger a performance impact. File an issue at bugs.webkit.org, so we can consider such feedback as we polish this feature before shipping in Safari itself.
When to use pretty vs balance?
Clearly, text-wrap: pretty is designed to make body text more beautifully typeset. But is that the only use case for it? What about text-wrap: balance? When should we use pretty or balance?
There are people who will give you an overly simple answer like “pretty is for paragraphs and balance is for headlines” — but that’s likely bad advice. Let’s look at what balance does in contrast to pretty and how to decide which one to use on headlines, captions, teasers, and other kinds of shorter, wrapped text.
text-wrap: balance
Basically, the text-wrap: balance rule tells the browser to wrap in such places to make every line be about the same length as the others. I think of it like folding a piece of paper into halves, or thirds, or quarters.
For example, here’s a headline with the default of text-wrap: auto. You can see that the word “enough” ends up as the first word of the second line, simply because there wasn’t enough space for it on the first line after “with”. Each line is laid out one-by-one, with no regard for the others. This causes this headline to end up with the word “itself” alone on the last line.
Here’s a similar headline with text-wrap: balance applied. There’s no longer a single word by itself on the last line. That’s great! But that’s not all. The last line is now the about same length as the other two. The first line is made significantly shorter so that its length is “balanced” with the length of the others. All three lines basically have the same length.
You’ll notice that as a result, the visual imprint of the headline / the chunk of “ink” on the page is now significantly narrower than the overall space available in its container (marked here with a cyan line).
This can be fantastic for your design. You can apply balance to headlines, captions, teasers, any shorter types of copy and it will have the same effect. It will stack the lines of text so they’re all about the same length — they are balanced. And once the text has been wrapped, it will likely not fill the box anymore. It will be narrower than the available space.
Sometimes, that’s not a desirable effect. Perhaps your headline sits under a teaser image, and the design calls for the text to be the same width as the image. You’ll notice that in this example, the first line ends up shorter than the rest. Balancing the text might come at too high a cost. Perhaps all you wanted to do is avoid a short last line.
You can instead use text-wrap: pretty on such headlines. This will avoid the short last line, while also still filling the container in the inline direction.
You can try out these examples yourself in Safari Technology Preview 126+ and Chrome/Edge 130+ to dive more into the effect of text-wrap on long, medium, and short headlines. Drag the corner of the boxes to see just how differently they handle wrapping.
What are the performance considerations for text-wrap: balance? Again, the CSS web standard leaves it to the browser engine to decide what kind of limits should be in place to ensure the users experience is not negatively impacted. Browsers do not have to make the same choices as each other.
The Chromium implementation limits the number of lines that are balanced to four to ensure Chrome, Edge and other Chromium browsers will still be fast. The WebKit implementation doesn’t need to limit the number of lines. Every line will be balanced with all of the others.
So if “pretty is for body text and balance is for headlines” is too simplistic a recommendation to be good advice, what might be a better way to think about the choice?
I think about it like this:
pretty can be applied to anything on the page — body text, headlines, captions, teasers, etc. Look to see what it does and if you like the effect. If you have incredibly long paragraphs (or better said: long body text without any paragraphs breaking it up, think in the hundreds or thousands of lines of text), test performance first. Also, if you are animating text in such a way that it rewraps as it animates, test to see if that’s a good idea.
balance should be used for anything where you want all the lines to be the same length — especially headlines and captions, etc. And where you do not mind if the overall group of lines is narrower than its container. Don’t use it on long passages of text; that doesn’t make sense.
auto is the default, which currently considers just one line at a time as layout is calculated, like the web has since 1991 (see below).
stable should be used for editable text and more (see below).
Unconvinced that text-wrap: balance won’t usually make sense on long passages of text? Well, you can try it out in this same demo.
See how the overall width of each paragraph is adjusted so that all the lines of text are about the same length, with no regard for how wide they are overall compared to their container? That’s what balancing does. And in the above example, that means each paragraph is a radically different width than the others. Which is odd. Only use it when you want that effect. Otherwise, you probably want to use another option.
What do the other values for text-wrap do? Let’s go through them.
text-wrap: avoid-short-last-lines
The avoid-short-last-lines value is the newest one in the CSS Text Module Level 4 specification. It’s not yet been implemented in any browser. It will focus on just avoiding short last lines, leaving pretty to do so much more.
text-wrap: auto
The default value of text-wrap currently does what the web has done since 1991, where each line of text is laid out by itself, with no consideration for multiple lines. (This is often called a “first-fit” or “greedy” algorithm.)
This, however, could change in the future! There may come a day when browsers decide to change the default, and apply some kind of multiline line-layout algorithm to all existing content on the web. This would improve all content, even old websites, even websites where the developer didn’t know about text-wrap: pretty.
text-wrap: stable
If you’ve tried text-wrap: stable, you might think “it doesn’t do anything! What is this?” Basically, right now, stable does the same thing as auto. It uses the original first-fit wrapping algorithm, where each line is laid out to fully fill that line with content, and only wrap where necessary.
This is an especially good choice of wrapping algorithms when the content itself is editable. If your user is writing text, you don’t want words/syllables jumping around, changing the wrapping as they type. To ensure your content won’t shift due to edits on subsequent lines, or in any case where you want OG line wrapping, apply text-wrap: stable.
This is also a good choice if you are animating text in such a way that it keeps re-wrapping. It will ensure the fastest wrapping algorithm is used at all times — important if the calculations are going to be done over and over in rapid succession.
By explicitly choosing text-wrap: stable you are ensuring this content will continue to wrap using the original algorithm, even if browsers redefine what auto does.
The text-wrap property is actually a short-hand for two longhands. The text-wrap-style property is for choosing the wrapping algorithm that’s used, while text-wrap-mode lets you turn wrapping on and off.
By having both the text-wrap-mode and text-wrap-style properties, you have the flexibility to change the style of wrapping independently from whether or not content wraps, and let these choices cascade independently.
This means you can also use the shorthand to simply turn off wrapping with text-wrap: nowrap. Or, use text-wrap: wrap to turn wrapping back on. Test out how it works in this demo.
Support for the text-wrap-mode and text-wrap-style longhands, along with the nowrap and wrap values, became “Baseline Newly Available” (aka, available in all major browsers) in October 2024, when Chromium added support in Chrome/Edge 130. To ensure full support for wrapping for people with older browsers, you can always provide a fallback to the older white-space: nowrap | normal. (Although when you do, take care to also check your white space collapsing behavior, since it’s affected by white-space.)
Update on what happened in WebKit in the week from March 31 to April 7.
Cross-Port 🐱
Graphics 🖼️
By default we divide layers that need to be painted into 512x512 tiles, and
only paint the tiles that have changed. We record each layer/tile combination
into a SkPicture and replay the painting commands in worker threads, either
on the CPU or the GPU. A change was
landed to improve the algorithm,
by recording the changed area of each layer into a single SkPicture, and for
each tile replay the same picture, but clipped to the tile dimensions and
position.
WPE WebKit 📟
WPE Platform API 🧩
New, modern platform API that supersedes usage of libwpe and WPE backends.
A WPE Platform-based implementation of Media Queries' Interaction Media
Features, supporting
pointer and hover-related queries, has
landed in WPE WebKit.
When using the Wayland backend, this change exposes the current state of
pointing devices (mouse and touchscreen), dynamically reacting to changes such
as plugging or unplugging. When the new WPEPlatform API is not used, the
previous behaviour, defined at build time, is still used.
Adaptation of WPE WebKit targeting the Android operating system.
A number of fixes havebeenmerged to fix and improve building WPE WebKit for Android. This is part of an ongoing effort to make it possible to build WPE-Android using upstream WebKit without needing additional patches.
The example MiniBrowser included with WPE-Android has beenfixed to handle edge-to-edge layouts on Android 15.
Safari Technology Preview Release 216 is now available for download for macOS Sequoia and macOS Sonoma. If you already have Safari Technology Preview installed, you can update it in System Settings under General → Software Update.
Fixed presentational images with empty alt attributes to be ignored by assistive technology, even when additional labeling attributes are set. (292105@main) (146429365)
CSS
New Features
Added additional support for text-wrap-style: pretty to now adjust all lines in the text box to improve rag and hyphenation. (291750@main) (146142653)
Resolved Issues
Fixed buttons to not have align-items: flex-start by default. (291883@main) (146615626)
Forms
Resolved Issues
Fixed form associated ElementInternals always reporting a customError when using setValidity. (291878@main) (115681066)
Media
Resolved Issues
Fixed stale audio buffer data after seeking when playing sound through an AudioContext. (291763@main) (146057507)
Rendering
Resolved Issues
Fixed integrating position-area and self-alignment properties for align-self and justify-self (291606@main) (145889235)
Fixed will-change: view-transition-name to create a stacking context and a backdrop root. (291636@main) (146281670)
Fixed will-change: offset-path to create a stacking context and a containing block. (291635@main) (146292698)
Service Workers
Resolved Issues
Fixed Service Worker downloads being prematurely interrupted. (291712@main) (143065672)
Fixed the ReadableStream cancel method not getting reliably called in Service Worker. (291551@main) (144297119)
SVG
Resolved Issues
Fixed SVG paint server fallback handling for a non-existent URL. (291726@main) (144493507)
Fixed respecting the CSS image-rendering property when drawing an SVG. (292202@main) (144507619)
Doing a 32-bit build on ARM64 hardware now works with GCC 11.x as well.
Graphics 🖼️
Landed a change that improves the painting of tile fragments in the compositor if damage propagation is enabled and if the tiles sizes are bigger than 256x256. In those cases, less GPU is utilized when damage allows.
The GTK and WPE ports no longer useDisplayList to serialize the painting commands and replay them in worker threads, but SkPictureRecorder/SkPicture from Skia. Some parts of the WebCore painting system, especially font rendering, are not thread-safe yet, and our current cross-thread use of DisplayList makes it harder to improve the current architecture without breaking GTK and WPE ports. This motivated the search for an alternative implementation.
Community & Events 🤝
Sysprof is now able to filter samples by marks. This allows for statistically relevant data on what's running when a specific mark is ongoing, and as a consequence, allows for better data analysis. You can read more here.
Safari 18.4 is here! It includes 84 new features, including a completely new declarative mechanism for sending Web Push notifications, lots of CSS including a brand-new shape() function, P3 & translucency for the HTML color picker, media formats with more robust support for recording, new Web APIs including modern popover manipulation, new JavaScript features like Iterators, a faster way to jump to device viewport presets in Responsive Design Mode, new Web Extension API, improvements to WKWebView, and enhancements across Networking, Storage, and Connection Security—plus much more.
A lot of our focus this winter has been on improving the quality and polish of existing web platform features, with a goal of greatly improving compatibility. WebKit for Safari 18.4 includes 184 resolved issues and 13 deprecations of older technology.
If you’ve struggled with support for something in WebKit or Safari in the past, please test again with WebKit for Safari 18.4. You can always file an issue about WebKit at bugs.webkit.org. Or, if the issue involves technology deeper in the stack, file feedback with a sysdiagnose to provide information on how the operating system itself is being impacted. If you run into a website that isn’t working as expected, please file a report at webcompat.com and our team will take a look. Filing issues really does make a difference. You can always ping our web evangelists: Jen Simmons on Bluesky / Mastodon, Saron Yitbarek on BlueSky, or Jon Davis on Bluesky / Mastodon.
Here’s a tour of what’s new with WebKit in Safari 18.4.
Declarative Web Push
Reaching users through push notifications is a powerful and important part of any modern computing platform. In 2013, Safari 7 on OS X 10.9 Mavericks added Safari Push, giving sites the ability to send push notifications across the web for the very first time. We learned a lot about how websites used this capability, and how users responded to it.
Other browsers were also eager to add push notifications to the web. The development of the Service Worker API coincided with the development of Push API, Notifications API, and RFC 8030. All four were combined into what we now call Web Push. Chrome first supported these standards in 2015 and Firefox in 2016.
The Safari team at Apple continued to refine Safari Push over the years, learning more about power usage, privacy, and its potential for abuse. The WebKit team at Apple watched the deployment of Web Push standards closely, then made deliberate decisions in implementing those standards in a privacy and power preserving way. That implementation shipped Web Push for Safari 16.1 on macOS followed by support for web apps on iOS and iPadOS 16.4.
The Web Push standards have required a Service Worker since the beginning, much like how web apps require Service Workers in Chromium browsers. Compared to the original Safari Push, which used a declarative model, requiring a Service Worker introduces added complexity for web developers. It also demands more from the system — consuming additional battery and CPU resources — and opens the door to potential misuse. Much like how web apps created from within Safari have never required a Service Worker on iOS, iPadOS, or macOS, we wanted the web platform to have push notifications that can also be declarative, displaying instantly without requiring a Service Worker.
Over the last several years we’ve been working on a new technology for push notifications on the web — Declarative Web Push. Learn all about how we designed and implemented it in Meet Declarative Web Push.
Declarative Web Push is now available on iOS and iPadOS 18.4 for web apps added to the Home Screen.
CSS
Shape function
For complex graphical effects like clipping an image or video to a shape, authors often fall back on CSS masking so that they can ensure that the mask adapts to the size and aspect ratio of the content being masked. Using the clip-path property was problematic, because the only way to specify a complex shape was with the path() function, which takes an SVG-style path, and the values in these paths don’t have units; they are just treated as CSS pixels. It was impossible to specify a path that was responsive to the element being clipped.
The CSS shape() function, new in WebKit for Safari 18.4, addresses these shortcomings. The shape() function is a new member in the family of CSS basic shapes, and provides a way to describe an SVG-style path as a list of path commands which use CSS units. For example, to create this simple shape:
WebKit for Safari 18.4 adds several improvements to the experience of using <details> and <summary> elements, continuing a slow march to significantly improve this feature. If you steered away from it in the past because of limitations, you should try it again. Web standards have evolved in recent years, and all browsers have been improving support as part of Interop 2025.
First, there is new support for the::details-content pseudo-element. You can use this pseudo-element to select just the content that appears when the details element is open, so you can style it independently.
This also means you can animate the height of this container for the first time, solving a long-standing problem that previously required JavaScript or an extra container around the content.
And WebKit for Safari 18.4 reimplements the <details> and </details><summary> disclosure triangle as a list item. This means you can now easily change the character used like this: summary { list-style: "+ "; } and further customize its styling using the::marker pseudo-element.
These changes modernize <details> and <summary>, making it possible to use this HTML instead of building custom controls from scratch using JavaScript. It’s faster and easier for you, and ensures your results have the proper semantics and full support for users of assistive technologies.
Sideways writing modes
Writing modes in CSS provide support for a wide variety of layout directions for written languages. At its core, the writing-mode property switches the text flow in the inline direction between a horizontal or vertical layout, as well as determining in which the direction in blocks stack. Safari has had support for writing-mode: horizontal-tb, vertical-rl, and vertical-lr since 2011 (prefixed until March 2017). Now, WebKit for Safari 18.4 adds support for writing-mode: sideways-rl and writing-mode: sideways-lr.
The sideways writing modes are very similar to vertical writing modes, but not the same. They were intended to be used for text that’s normally laid out horizontally to be instead displayed vertically as a graphic design effect. Note in the demo below the difference between how CJK characters are rendered in sideways-* vs vertical-*. When the intention is to layout out typical horizontal text sideways, using sideways-rl instead of vertical-rl ensures punctuation and other direction-neutral characters are typeset correctly for the purpose at hand. It also handles the baseline of the text differently. Using sideways-lr instead of vertical-lr creates an entirely different result. Dig into this example to see more, including toggling to see the differences in text wrapping.
WebKit for Safari 18.4 adds support for the brand new text-autospace property, which automatically introduces extra space to provide visual breathing room when transitioning between scripts. In the distant past, Chinese and Japanese were written in a purely native writing system derived from ancient Chinese Han characters. But in modern typesetting, they liberally mix in Western numbers and letters. When set solid, the boundary between the traditional characters and the new foreign characters feels cramped, and so modern typesetting convention has adopted a practice of inserting a little bit of extra space at these boundaries. With the text-autospace property, the browser automatically inserts this extra space whenever it detects a script transition.
The options for values include:
ideograph-alpha creates extra spacing between runs of CJK and non-CJK letters.
ideograph-numeric creates extra spacing between runs of CJK and non-CJK numerals.
normal does both ideograph-alpha and ideograph-numeric at the same time.
WebKit defaults to text-autospace: no-autospace to match the current default behavior of older (currently all) browsers. The CSS specification calls for browsers to switch the default to text-autospace: normal — automatically applying better spacing to all CJK content on the web, no matter when the website was created.
We have not yet switched to the new default behavior. Use text-autospace In Safari 18.4 to opt your content into the new spacing, and test it out. If you have thoughts about the implementation, file an issue at bugs.webkit.org. We want to ensure our implementation is well tested before changing the default.
View Transitions
Last December, Safari 18.2 added support for view-transition-name: auto, allowing you to avoid individually naming potentially hundreds of different content items if you are applying transitions to the content on a single page. With auto , the id is first checked to be identical across the transition, then if the id isn’t present, the elements are checked directly to be identical across the transition.
In WebKit for Safari 18.4, we’re adding support for view-transition-name: match-element, which only checks the elements to be identical across the transition. This can only be used for single-page view transitions, whereas auto can be used for multi-page view transitions through matching the id attribute.
And more CSS updates
There’s also added support for several more CSS features in WebKit for Safari 18.4:
gradients with only one stop
fallbacks inside attr(), for example — attr(data-count, "0")
unicode-bidi text rendering UA rules (except for ruby elements)
And the non-standard CSSUnknownRule interface has been removed.
HTML
Now, WebKit for Safari 18.4 enhances <input type="color"> to support alpha and colorspace attributes. This means now you can offer users the chance to choose a color from the Display P3 colorspace, and/or to adjust the opacity of their choice with <input type="color" colorspace="display-p3" alpha>.
You can also use any supported CSS color syntax inside the value attribute of the <input type="color" /> control and it will be properly converted as per the colorspace attribute.
And the WebKit team worked with the WHATWG community to standardize these color picker enhancements in the HTML standard.
WebKit for Safari 18.4 adds iOS support for thewebkitdirectory attribute on <input type="file" /> elements. When a file input with the attribute is invoked, the document picker is immediately displayed with non-directory items grayed out. From this document picker, a directory can be uploaded by navigating into the target directory and pressing “Open”.
Support for the composite attribute on an <img> element has been removed in WebKit for Safari 18.4. It was originally added in April 2004 to support composite operations on images (combining two images for creating an effect) in the context of the now discontinued Dashboard macOS feature. This attribute is not necessary anymore and can be safely removed.
Web Inspector
Responsive Design Mode in Safari 18.4 allows you to select from a list of device viewport size presets to quickly test the layout of your web page. Presets can be rotated for portrait and landscape orientations. Viewport size presets offer a good approximation of how your web page will be affected by viewport sizes, but they don’t represent exact layout, rendering, and behavior as experienced on an actual device. For example, the page layout on a device might be influenced by the browser address bar or the on-screen keyboard. To get high fidelity previews, use the Open with Simulator menu to check the webpage on a device simulator.
If you use JSContexts in your macOS, iOS, iPadOS, visionOS, watchOS or tvOS app, you can use Safari Web Inspector to debug them. Sometimes, you need to automatically inspect and pause script execution in a JSContext before it has a chance to run so you can set breakpoints and manually step through the code. Prior to Safari 18.4, you could only configure these options for the entire device which could lead to interruptions as JSContexts from other apps would also be automatically inspected. Starting in Safari 18.4, you can configure automatic inspection and pausing of JSContexts just for your app.
The new Inspect Apps and Devices menu item in the Develop menu opens a window with a list of all connected devices and currently running apps that have inspectable content, such as webpages, service workers, JSContexts, WKWebViews, and web extension background pages. Next to each app in the list, there is a triple-dot menu which reveals options to configure automatic inspection and pausing of JSContexts just for that app. The settings apply to any new JSContext created by that app. If the JSContext you want to automatically inspect is created at app launch, you’ll need to restart the app to see the effect.
You can use a Request Local Override to add or change headers for requests matching a URL without modifying any of the other data by ticking the checkbox for “Include original request data” in the Local Override configuration popover. Any new or changed headers will augment the request headers. Starting in Safari 18.4, you can select the “passthrough” option for the Method selector to apply the override for any HTTP method.
The User Agent string overrides list now includes Android options. You can apply an override for the currently open page using the User Agent submenu from the Safari Develop menu. When remotely-inspecting a web page on a connected device or simulator, you can find User Agent string overrides in the Device Settings popover.
The console.screenshot method from the Web Inspector Console API now supports providing a DOMRect as an argument to capture a screenshot of a precise area of the web page.
Web Inspector now supports the ignoreList field from the official source map specification. Tools that generate source maps can use this field to identify sources that can be ignored, for example framework code or bundler-generated code, to ease the cognitive burden for developers when debugging their own code.
The Cookie table view in the Storage tab can now be configured to show a column with the cookie partition key for partitioned cookies.
Media
WebKit for Safari 18.4 adds support for Image Capture API. It provides a way to enable the capture of images or photos from a camera or other photographic device through MediaStream Image Capture API.
As part of our efforts to improve web compatibility, MediaRecorder in WebKit for Safari 18.4 now supports creating WebM files using the Opus audio codec and either VP8 or VP9 for video. Additionally, it can now create ISOBMFF (fragmented MP4) files, which can be easily used with Media Source Extensions’ SourceBuffer. MediaRecorder can also generate high-quality, lossless audio tracks in ALAC or PCM formats. And we’ve added support for video tracks in H264, HEVC, and AV1 (for devices with AV1 hardware support).
WebKit for Safari 18.4 rounds out our support for media formats by adding Ogg container support for both Opus and Vorbis audio on macOS Sequoia 15.4, iOS 18.4, iPadOS 18.4, and visionOS 2.4.
WebRTC
WebKit for Safari 18.4 adds WebRTC support for the MediaSession capture mute API:
Web pages can detect in a central place whether user muted/unmuted camera/microphone/screenshare capture via specific actions
Web pages can ask capture to be muted/unmuted via dedicated methods. Unmuting requires user activation and will not trigger a user prompt if muting was done by the web page.
Speakers can be enumerated once microphone access is granted
Audio rendered with HTMLMediaElement can be routed to specific speakers via setSinkId.
SVG
There are a few updates to SVG in WebKit for Safari 18.4.
There’s new support for the lh and ch units inside of SVG. Note that support for the ch does not include support for upright vertical character width.
SVGImageElement.prototype.decode() is now supported to help you avoid an empty image while waiting to download and decoding an SVG image.
And WebKit for Safari 18.4 removes support for the SVG 1.1 kerning property, and the SVGDocument alias to XMLDocument.
Web API
The Screen Wake Lock API now also works in Home Screen Web Apps on iOS and iPadOS 18.4. This allows you to prevent a device from dimming and locking the screen. It’s especially great for use cases like recipe apps, when the user is still reading but not touching the screen.
The dialog.requestClose() method is new with WebKit for Safari 18.4. Use it to request to close a <dialog>. It differs from the other methods by firing a cancel event before firing the close event and closing the dialog.
WebKit for Safari 18.4 adds an option to set an invoker for popover from an imperative API with the showPopover() and togglePopover() methods. For example:
The Cookie Store API is a new asynchronous API in WebKit for Safari 18.4 for managing cookies and getting notifications about changes. Due to privacy concerns, this new API exposes only the “name” and “value” properties of cookies—just like document.cookie. The change event is implemented for windows, but not yet for service workers due to ambiguities in the specification.
Compression Streams now supports compressing and decompressing data using the Brotli format. This may offer significant improvements in both performance and compression size based on the specific data compared to Deflate. Authors only need to update the CompressionStreams constructor to brotli to take advantage of the new feature.
WebKit for Safari 18.4 adds support for X25519 for Web Cryptography which allows access to deriveBits, deriveKey, exportKey, generateKey, and importKey in the SubtleCrypto interface. This offers better security and less reliance on external libraries to get the benefits of secure curves.
Key generation, import and export support for CryptoKeyOKP(x25519/ed25519) is now supported with a CryptoKit implementation, allowing use of Curve25519 in cryptography.
New control of the focusing process is available in WebKit for Safari 18.4 with support for element.focus({ focusVisible: true }). This allows developers to programmatically control the visible focus indicator of an element, to force it to be visible or prevent it from being visible instead of relying exclusively on the User Agent.
WebKit for Safari 18.4 adds support for Scroll To Text Fragment feature detection with document.fragmentDirective. This lets you test whether or not text fragments are supported in your browser by checking for existence of the object.
WebKit for Safari 18.4 removes support for built-in wheel event handling for <input type="number" />. Its behavior did not match the equivalent control on Apple platform’s and was confusing to end users. Web developers frequently sought to disable it, or would enable it inadvertently.
JavaScript
Iterators
WebKit for Safari 18.4 adds support for several new features from the Iterator Helpers proposal. When interacting with large sets of data it might not be possible to fit all your data in memory at once, such as in an Array. With iterator helpers, data is handled lazily so it doesn’t all have to be in memory at the same time.
As an example, Around the World in Eighty Days is a French novel by Jules Verne. The hero, Phileas Fogg, makes a bet that he can navigate around the world in no more than 80 days. These are the cities Phileas Fogg will go through:
function*cities() {
yield"London (UK)";
yield"Suez (Egypt)";
yield"Bombay (India)";
yield"Calcutta (India)";
yield"Victoria (Hong-Kong)";
yield"Singapore (Singapore)";
yield"Yokohama (Japan)";
yield"San Francisco (USA)";
yield"New-York (USA)";
yield"London (UK)";
// If Phileas went to every city in the world this list would be very long.
}
We can make an iterator out of the cities generator function by calling it. For other iterable data structures such as Map, Set, and Array we can use Iterator.from.
One of the nicest parts of iterator helpers is that you can compose them. Let’s say we want to know how many places Phileas visited in India on his journey. We can use the reduce function on our filtered iterator to get our answer.
The method Iterator.map() applies the function for each elements returned by the iterator when .next() is called.
citiesIter=cities();
// here we create a function modifying the element returned by `next()`.
travelDone= (x); "Travel to "+x+": done!";
letcitiesDone=citiesIter.map(travelDone);
citiesDone.next();
// {value: "Travel to London (UK): done!", done: false}
citiesDone.next();
// {value: "Travel to Suez (Egypt): done!", done: false}
// etc.
WebKit improves parsing performance using SIMDe in JSON for fast scanning of strings in JSON.parse, plus fast scanning and copying of strings in JSON.stringify. SIMD (Single Instruction, Multiple Data) is practical when handling computations on a large set of data, where each data element receives the same instruction.
Improved Error detection
WebKit for Safari 18.4 supports Error.isError to identify a “real” native error in a world where Symbol.toStringTag means there isn’t a reliable way to test the internal slot for Error instances. It makes it possible to normalize error objects instead of relying on only strings.
try {
throw"Oops; this is not an Error object. Just a string.";
} catch (e) {
// test if it's not an error
if (!Error.isError(e)) {
// make it a real error
e=newError(e);
}
console.error(e.message);
}
And more
WebKit also allows for programs using spin-wait loops to give the CPU a hint that it’s waiting on a value and still spinning by implementing Atomics.pause.
Canvas
WebKit adds un-prefixed letterSpacing and wordSpacing for CanvasRenderingContext2D in Safari 18.4. This enables you to specify the spacing between words and letters when drawing text in a 2D canvas, without a -webkit prefix.
And support has been removed for three antiquated technologies to improve interoperability and compatibility:
webKitBackingStorePixelRatio
the prefixed webkitImageSmoothingEnabled — use the standard imageSmoothingEnabled property instead
the non-standard legacy alias of Canvas Compositing including setAlpha and setCompositeOperation
Editing
WebKit for Safari 18.4 adds support for ClipboardItem.supports(). When a web application needs to place content in the clipboard of the operating system, it is useful to know in advance if the format is supported by the clipboard. By default, browsers support text/plain, text/html and image/png .
This gives the ability to check if other formats are supported. It will send back false when the format is not supported and avoid an error message.
It also adds text/uri-list which is a supported format by Safari, useful when sharing list of URLs.
It also improves interoperability by sending TypeError for a new ClipboardItem() with an empty Array.
Also, WebKit for Safari 18.4 fixes document.execCommand("copy") so that it can be triggered even if there is not any text selected.
Loading
With Safari 18.4, WebKit now supports noopener-allow-popups in Cross-Origin-Opener-Policy (COOP). This disconnects the relationship between the opener and the document loaded with its policy, but still allows the document to open other documents if their COOP policy allows it.
WebAssembly
WebKit for Safari 18.4 supports running Wasm when Just-In-Time compilation (JIT) is disabled. That means Safari can still run Wasm in environments where JIT compilation has been turned off to boost system security.
There’s also support for the new Wasm Exception specification. Exception handling allows code to break control flow when an exception is thrown. The new specification provides both a mechanism for Wasm code to handle JavaScript exceptions, and to throw its own exceptions. This new specification supersedes the legacy proposal, which WebKit supported since Safari 15.2 and will continue to support for compatibility.
WebKit for Safari 18.4 adds support for relaxed_laneselect SIMD instructions which means that when the mask is all its bits set or unset, it will behave like a bitwise select SIMD instruction. Otherwise, if the mask has a different value, the implementation can then use the backing architecture to pick the relaxed instruction’s behavior.
Web Extensions
Browser Web Extension APIs
WebKit on iOS 18.4, iPadOS 18.4, visionOS 2.4, and macOS Sequoia 15.4 adds support for integrating web extensions into WebKit-based browsers with a set of straightforward Swift and Objective-C APIs. With the new WKWebExtension, WKWebExtensionContext, and WKWebExtensionController classes, browsers can incorporate web extensions that empower users to customize their browsing experience. Integrating web extensions into WebKit enables all WebKit-based browsers to align on a unified implementation, ensuring they all benefit from continuous improvements and fixes in support of the evolving web extensions standard.
Temporary Extension Installation
New in Safari 18.4 on macOS, you can temporarily install a web extension from disk. This provides a convenient way to develop your extension and test compatibility without building an Xcode project. When loading a temporary extension, you’ll have access to most Safari Web Extension functionality. When you’re ready to test out nativeMessaging, or prepare your extension for distribution, you can create a new Xcode project using the Safari Web Extension Converter.
Developer ID-Signed and Notarized Safari Web Extensions
Safari 18.4 on macOS now supports Safari Web Extensions that have been Developer ID-signed and notarized. Notarization is an additional step after signing your app and extension with your Developer ID. Apple’s notarization service automatically checks for malicious content and code-signing issues. Once notarized, your app and extension is ready for distribution and use in Safari.
Reliable Document Identification
Safari 18.4 adds support for documentId in webRequest, webNavigation, tabs, and scripting APIs, ensuring extensions can reliably track documents when sending messages, injecting scripts, or processing requests. Unlike frameId, which stays the same when a new document loads in the same frame, documentId updates with each navigation, helping extensions avoid interacting with the wrong content.
Efficient Storage Key Retrieval
Safari 18.4 introduces getKeys() in extension storage, allowing developers to retrieve stored keys without fetching their associated values. This improves performance for extensions that manage large sets of structured data, such as those that store user preferences, session data, or categorized content. By retrieving only the keys, extensions can efficiently determine what data is available before deciding which values to load, reducing unnecessary data transfers.
Improved Localization Support
Support for i18n.getSystemUILanguage() and i18n.getPreferredSystemLanguages() in Safari 18.4 provides extensions with access to the user’s system language settings. Unlike i18n.getUILanguage(), which returns the browser’s interface language, these APIs allow extensions to align with system-wide language preferences. This is particularly useful for extensions that format dates, numbers, and other locale-specific content according to the user’s preferred regional settings, even when the browser’s locale differs.
Expanded Subframe Injection
Safari 18.4 adds support for match_about_blank and match_origin_as_fallback in extension manifests, as well as matchOriginAsFallback in scripting for dynamically registered content scripts. These properties allow scripts and styles to run in additional frames such as about:blank, data:, and blob: URLs by matching based on the origin of the parent frame rather than the frame’s own URL, making it possible to target frames with opaque origins. Host permissions for the parent frame’s origin are still required.
WKWebView
In this release, we brought more platform parity across our APIs. On iOS, it’s now possible for API clients to customize the upload flow for file inputs using the WKUIDelegate/webView(_:runOpenPanelWith:initiatedByFrame:) delegate method.
We also now expose the buttonNumber and modifierFlags properties of WKNavigationAction on iOS and visionOS.
Lastly, visionOS 2.4 brings Apple Intelligence features like summarization, text compose, rewriting, and proofreading into WebKit client experiences on visionOS with support for Writing Tools. This includes their respective APIs, WKWebView.isWritingToolsActive and WKWebViewConfiguration.writingToolsBehavior.
Networking
WebKit for Safari 18.4 on macOS Sequoia 15.4, iOS 18.4, and visionOS 2.4 introduce support for opt-in partitioned cookies, known as CHIPS. Partitioned cookies allow third-party content on a web page to create and access cookies on that specific site without allowing cross-site tracking. CHIPS is an important technology for web sites that still require access to cookies in a third-party context (e.g., an iframe), and it is an important step toward helping sites finish their migration away from relying on unpartitioned, cross-site cookies.
For example, if https://siteA.example creates an iframe and loads https://siteB.example in it, and the webpage from https://siteB.example creates a partitioned cookie, then that cookie is only accessible to siteB.example on a webpage from siteA.example. Note that WebKit is using a different partitioning boundary for CHIPS compared with its other storage areas (e.g., localstorage). WebKit is aligning with the other browser engines by partitioning cookies by site, as compared to by origin. This means that if both https://news.siteA.example and https://mail.siteA.example embed an iframe from https://chat.siteB.example, then https://chat.siteB.example will have access to the same cookie on both web pages because https://news.siteA.example and https://mail.siteA.example are subdomains of the same site: https://siteA.example.
Partitioned cookies with CHIPS require that the website explicitly sets a new attribute on the cookie. This explicit attribute ensures that WebKit and the website have a shared understanding that the cookie will be only accessible on a particular site. The new attribute is named Partitioned, and it is used in a similar manner to the Secure or HTTPOnly attributes. As with other cookie attributes, the Partitioned attribute is only used when the cookie is set, and it is not accessible afterward.
As an example, if you want to set a partitioned cookie using the HTTP Set-Cookie header, that could look like:
Note, this cookie includes the SameSite=None and Secure attributes. These attributes are required for a Partitioned cookie, and the cookie will be blocked if those attributes are not set. This means that the cookie much be created and accessed from a secure webpage (e.g., https) and the cookie will be sent in all go your cross-site requests to your server. You can read more about these attributes on the linked pages.
Similar to setting a partitioned cookie with the HTTP header, you can set it using JavaScript, as well. For example, JavaScript could create the same cookie as above:
And document.cookie will return "TestCookie=12345".
Note, cross-site tracking domains may not be allowed to use partitioned cookies.
If you require access to both partitioned third-party cookies and unpartitioned third-party cookies, then you can continue using the Storage Access API with document.requestStorageAccess() to request access to the unpartitioned cookies.
Storage
WebKit for Safari 18.4 adds support for clearing partitioned cookies using the Clear-Site-Data HTTP header. Support for clearing first party cookies using the Clear-Site-Data was introduced in Safari 17.0. Now, with the introduction of CHIPS, described above, this HTTP header has different behavior depending on which server sends it. If the header is received in the response that was sent by the first party site (e.g., the main web page), then WebKit will clear only unpartitioned cookies for that domain. If the HTTP header is received in a response that was sent by a cross-site iframe, then WebKit will clear only partitioned cookies for the particular site on which the iframe is loaded.
Connection Security
For years, the lock icon in Safari indicated that the connection is secure — that the site is using HTTPS. With more than 87% of all connections made over HTTPS now, secure connections are ubiquitous. The new norm. Meanwhile, the presence of the lock could be creating a false sense of trustworthiness, if users instead believe it’s there to signal the website is trustworthy. With this in mind, we removed the lock icon from the Smart Search field for HTTPS connection in Safari 18.4.
For users who would like the ability to learn more about connection security, we’re introducing a new option. On macOS, go to Safari menu; Connection Security Details. And on iOS, iPadOS, and visionOS, you’ll find the information in Page menu; more; Connection Security Details.
This view lets users confirm the connection is secure, and for the first time on iOS, iPadOS, and visionOS, view certificate information such as CA issuer and expiration date.
In the EU, Connection Security Details indicate connections made using an EU Qualified Web Authentication (QWAC) certificate providing an indicator for enhanced trust and security for online interactions on iOS 18.4, iPadOS 18.4, macOS Sequoia 15.4, and in visionOS 2.4.
Additionally, WebKit for Safari 18.4 changes 3DES cipher to show a warning to users that it’s a legacy TLS (Transport Layer Security). Tap here to see such a warning.
Security
WebKit for Safari 18.4 adds support for CSP Hash Reporting keywords: report-sha256, report-sha384 and report-sha512. And it removes support for Clear-Site-Data: for executionContexts since Safari was the most only browser with support.
WebKit for Safari 18.4 now enables a user to set a PIN for a security key when it’s required during registration.
Resolved Issues
In addition to new features, WebKit for Safari 18.4 includes work to polish existing features.
Browser
Fixed an issue where sites would log out automatically after a brief time. (99829958)
CSS
Fixed table border-color to be currentColor by default. (48382483)
Fixed combining CSS clip-path with any property that creates a new stacking context makes <img> element disappear. (86091397)
Fixed resize to not be applied to generated content. (121348638)
Fixed ensuring the correct logic is run for over-constrained cases when the absolute positioned box is a writing-mode root. (142214631)
Fixed animation-name set from the view transitions dynamic UA stylesheet having extra quotes. (142298840)
Fixed the serialization and parsing of animation-name strings. (142318879)
Fixed text-box-trim accumulation failing when updating the CSS dynamically. (142386761)
Fixed text-emphasis to not paint emphasis marks on punctuations. (142387538)
Fixed sizing and positioning issues when a popover changes CSS position upon opening. (142491219)
Fixed Page Zoom (⌘+ and ⌘-) to work with calc() used with font-size on macOS. (142736427) (FB16287129)
Fixed scroll-padding and scroll-margin to be strongly typed CSS/Style values. (142830546)
Fixed View Transitions to stop running when the user navigates with a swipe. (142844150)
Fixed top-level and nesting selector to have zero specificity matching a recent specification update. (143765827)
Editing
Fixed document.execCommand("copy") only triggering if there is a selection. (27792460)
Fixed an issue where iCloud Notes pasted text copied from a plain text document in Safari as raw markup. (124788252)
Fixed highlighting correctly a large text selection that ends with a common phrase. (135973065)
Fixed copying a link to a common term in an article to highlight the correct part of the page. (135973186)
Fixed missing SecureContext in the ClipboardItem interface. (137197266)
Fixed Hebrew text pasted from Safari getting aligned left. (139029945)
Fixed setting selection to not set focus unless there is an existing selection. (139075809)
Fixed sometimes being unable to select text for non-editable content. (143296175)
Fixed missing selection handles after selecting text across multiple lines. (143720155)
Forms
Fixed <textarea> to handle switching direction. (73475239)
Fixed setting a datetime-local input to a large value cause a crash. (135733092)
Fixed <datalist> dropdown keyboard interactions to align with platform conventions. (143012287)
Fixed: Disabled all Writing Tools app menu items, except “Compose”, for empty editable content. (143332082)
Home Screen Web Apps
Fixed Screen Wake Lock API for Home Screen Web Apps. (108573133)
HTML
Implemented <details> and <summary> disclosure triangle as a list item. (95148788)
Images
Fixed broken WebP images in lockdown mode. (144224372)
JavaScript
Fixed Array destructuring assignment to close the iterator if an evaluation throws an exception. (121960887)
Fixed: Updated Intl.DurationFormat#resolvedOptions to the latest specification. (136276429)
Fixed Iterator Helpers methods to not iterate an array. (136303997)
Fixed Iterator.prototype.reduce() not properly forwarding the return() call to the underlying iterator. (137181340)
Fixed Set.prototype methods to invoke keys() without arguments. (137395979)
Fixed Array.from(), Array.fromAsync(), and TypedArray.from() to invoke document.all passed as a mapper. (137490201)
Fixed Intl.DurationFormat to have a value limit to match the specification. (137885273)
Fixed a rounding error for Intl.DurationFormat. (138261569)
Fixed calendar canonicalization logic in DateTimeFormat. (141792829)
Fixed broken output for Intl.DurationFormat digital style when hoursDisplay is "auto". (141969050)
Fixed Intl.DurationFormat to print a negative sign for minutes after hidden hours. (142119353)
Fixed Array.prototype.toReversed to fill holes with undefined. (142197604)
Fixed: Increased the matchLimit for regular expressions, allowing complex matches on longer strings. (143202375)
Media
Fixed handling an empty srcAttr in Media Element. (132042925)
Fixed getUserMedia video track getSettings() returning a stale value for torch and whiteBalanceMode constraints. (137870391)
Fixed the space key not pausing a video in fullscreen by making the video mouse focusable. (138037616)
Fixed an issue where playback doesn’t always resume after a seek. (140097993)
Fixed playing video generating non-monotonic ‘timeupdate’ events. (142275184) (FB16222910)
Fixed websites calling play() during a seek() is allowed by the specification so that the play event is fired even if the seek hasn’t completed. (142517488)
Fixed seek not completing for WebM under some circumstances. (143372794)
Fixed MediaRecorderPrivateEncoder writing frames out of order. (143956063)
Networking
Fixed optimistically upgraded navigations to set a timeout based on current network conditions. (135972599)
PDF
Fixed switching a PDF from continuous to discrete mode displaying the page(s) that are at the top of the window, even when barely visible. (137608841)
Fixed the “Previous Page” context menu option not navigating to previous page in 2-up continuous mode. (139817364)
Fixed main frame PDFs served with a CSP sandbox header not loading. (141166987)
Rendering
Fixed computing the baseline for replaced elements with an intrinsic ratio but no intrinsic size as flex items. (74279029)
Fixed flickering caused by extra resize events dispatched when rotating from landscape to portrait on iOS. (93767145)
Fixed adding out-of-flow objects under the inline in a continuation chain, when possible. (102421379)
Fixed mix-blend-mode to work for large resolution fixed or stick elements. (104686540)
Fixed the missing table collapsed border for <thead>, <tbody>, and <tfoot> elements in the wrong order. (110430887)
Fixed <input type="range"> taking up space even with width: 0 applied. (113402515)
Fixed the Spotify media player disappearing when rotating to landscape mode on iOS. (123870311)
Fixed textarea elements to reserve space for overlay scrollbars. (129597865)
Fixed grid layout animation performance by caching intrinsic logical heights during the first row sizing pass, improving efficiency and preventing invalidation issues with complex grid configurations. (135791322)
Fixed Grid item which is an image with specified sizes failing to update when the src changes. (135972911)
Fixed nested inlines’ vertical alignment when line-fit-edge is set. (136036997)
Fixed the consistency of table layout with <td width="100%">. (136090741)
Fixed repainting to be more consistent for text-underline-position. (136095297)
Fixed floats not clearing in the WordPress Classic Editor sidebar layout. (136362683)
Fixed handling of out-of-flow children in MathML layout functions to be consistent. (136683070)
Fixed a repeating background-image sized to the content-box failing to fill the viewport in an iframe. (136725820)
Fixed consistently triggering a reflow when needed for table DOM manipulations. (137300794)
Fixed scriptlevel multipler for font-size in MathML. (137671252)
Fixed inline marquees to allow them to shrink when adjacent to float(s). (137766071)
Fixed a flex container with no flex item to not run flex layout. (137884128)
Fixed support for CSS width and height properties on MathML elements. (138174295)
Fixed incorrectly overlapping when a float has shape-outside: inset. (139133291)
Fixed right-to-left content failing with a shape-outside float. (139198865)
Fixed incorrectly overlapping a float that has shape-outside: ellipse in vertical mode. (139208636)
Fixed incorrectly overlapping a float that has shape-outside: polygon in right-to-left. (139215719)
Fixed outside list-style-position quirk to only be applicable in quirks mode. (140602985)
Fixed: Updated line box dimensions. (141167251)
Fixed incorrect horizontal writing mode state when nested in a vertical block container. (141543326)
Fixed baseline calculation few cases for tables with empty rows. (142046863)
Fixed to refuse to break inside replaced content. (142224455)
Fixed absolute positioned child with percent to include containing block padding. (142321535)
Fixed computing an out-of-flow box width correctly when it is inside an inline continuation. (142417374)
Fixed a border not showing when a linear gradient and a border radius are set. (142617573)
Fixed relative-positioned input elements in scroll areas not rendering outlines. (142995142)
Fixed tabbing out of a popover causing a hang in certain cases. (143145544)
Fixed setting up inline continuations correctly when not inserting a new child. (143388080)
Fixed adding a margin-top to a <rt> also adds a bottom margin. (143720832)
Scrolling
Fixed changes to the “scrolling” attribute on an iframe element already in the DOM to take effect. (98911472)
Service Workers
Fixed handling the case of busy looping service workers in a process containing web pages. (138626537)
Fixed an unexpected failure when serving a redirected response from cache for a navigation loaded via service worker navigation preload. (146113615)
Storage
Fixed the Storage Access API to consider AllExceptPartitioned as not currently having cookie access, ensuring sites can request access to first-party cookie. (143508260)
SVG
Fixed not propagating the bounding box for empty text to ancestors. (115217040)
Fixed SVG masks not working as a mask-image. (127327715)
Fixed a bug in case of reference elements (e.g., textPath) unable to notify the referring element (e.g, text) about their availability. (135509733)
Fixed SVGUseElement to prevent sniffing the content type when loading an external document. (135972621)
Fixed vertical writing modes to se the correct bounding rect. (135973175)
Fixed: Updated getTotalLength() with the web specification to throw an exception when non-renderable and the path is empty. (136719548)
Fixed not picking the “zh” locale when “zh-Hant” is preferred. (142602243) (FB16271745)
Fixed webRequest event listeners to honor extraInfoSpec for better performace. (142907168)
Fixed web extension resources to be treated as UTF-8 by default. (143079179)
Web Inspector
Fixed ensuring that all of the Desktop Sites on iPad site-specific hacks are disabled when the site-specific hacks setting is turned off in Web Inspector. (50035167)
Fixed style rules to stay editable after being modified by CSSOM in JavaScript. (124650808)
Fixed glitches when trying to edit a style from a stylesheet that has an @import statement. (131756178)
Fixed error cases to match new source map specification. (137934436)
Fixed the overview icon to be inverted dark mode in the Graphics tab. (140602803)
Fixed recorded WebGL objects not getting highlighted correctly in the Graphics tab. (140625113)
Fixed a crash that could occur when simulating drag events with the right mouse button. (137068514)
WebRTC
Fixed MediaSession.setMicrophoneActive(true) prompting repeatedly if the microphone was muted by the user-agent once. (135941062)
Fixed setCameraActive to not unmute microphone if the user-agent previously muted both camera and microphone. (136221456)
Fixed AirPods unmuting to not unmute the camera if website muted the camera. (137065964)
Fixed voice search to not re-prompt for camera or microphone permission after a page-initiated same origin navigation. (138122655)
WKWebView
Fixed calling WKWebView.evaluateJavaScript in an async context when nothing is returned by JS. (139618495) (FB15755273)
Updating to Safari 18.4
Safari 18.4 is available on iOS 18.4, iPadOS 18.4, macOS Sequoia 15.4, macOS Sonoma, macOS Ventura, and in visionOS 2.4. To get the latest version of Safari on iPhone, iPad or Apple Vision Pro, go to Settings; General; Software Update, and tap to update.
If you are running macOS Sonoma or macOS Ventura, you can update Safari by itself, without updating macOS. Go to ; System Settings; General; Software Update and click “More info…” under Updates Available.
Feedback
We love hearing from you. To share your thoughts, find our web evangelist online: Jen Simmons on Bluesky / Mastodon, Saron Yitbarek on BlueSky, and Jon Davis on Bluesky / Mastodon. You can follow WebKit on LinkedIn. If you run into any issues, we welcome your feedback on Safari UI (learn more about filing Feedback), or your WebKit bug report about web technologies or Web Inspector. If you run into a website that isn’t working as expected, please file a report at webcompat.com. Filing issues really does make a difference.
Download the latest Safari Technology Preview on macOS to stay at the forefront of the web platform and to use the latest Web Inspector features.
CSS Grid and Flexbox brought incredible layout tools to the web, but they don’t yet do everything a designer might want. One of those things is a popular layout pattern called “masonry” or “waterfall,” which currently still requires a Javascript library to accomplish.
Masonry layouts in CSS feature was first proposed by Mozilla and implemented in Firefox behind a flag in 2020. From the beginning, there’s been a hot debate about this mechanism. The WebKit team at Apple picked up where Mozilla left off, implemented the drafted spec, and landed it in Safari Technology Preview 163. This reinvigorated the conversation. By October 2024, there were two competing ideas being debated — to “Just Use Grid” or to create a whole “New Masonry Layout”. We wrote extensively about these ideas in a previous article on this website.
A lot has happened since October. Now, a third path forward is emerging — a path that would mean the CSS Working Group doesn’t choose either “Just Use Grid” or “New Masonry Layout”. It merges ideas from both with a completely-new idea to create a unified system of Item Flow properties. This article explains what Item Flow is, and its impact on both Flexbox and Grid. In Part 2, another article will more fully explain the implications for the future of masonry-style layouts.
But first, why is a third possibility called Item Flow emerging at all? Why not just pick between the “Just Use Grid” and “New Masonry Layout” options? Well, back in October, the folks working on masonry asked the W3C Technical Architecture Group (TAG) to weigh in on the debate. The TAG had a lengthy response, but one of the most interesting parts was this:
Overall, we think masonry, grid, and wrapping-flexbox should be incorporated into a unified set of properties. Chrome’s [New Masonry Layout] proposal splits apart property sets too eagerly, but even the WebKit [originally Mozilla | Just Use Grid] proposal seems to miss a chance to develop more-general properties.
Wow. Incorporate Flexbox, Grid and Masonry into a unified set of properties? What would that even look like?
The suggestion isn’t to combine all of Flexbox with all of Grid — but rather to create a new set of properties and values to “replace” specifically the flex-flow and grid-auto-flow properties. (You will always be able to use the older syntax if you prefer.) Think how originally the Alignment properties and thegap property were defined to work only in one layout system, and then later got modified and extended to work in multiple layout contexts.
Several of us here at Apple got together and started figuring out how a such unified system for layout *-flow could work. We decided to tentatively name the shorthand item-flow. It’d be the main property from which the longhands and values follow. Together, the new rules would all control the direction of flow, how items wrap, how items pack, whether or not there’s “slack” in the layout, and more.
As we worked through the details, we started to get excited. Suddenly new features for Flexbox and Grid that people have wanted for years had an obvious home. Things seemed to click together elegantly. New capabilities emerged:
Flexbox could gain a way to do dense packing
Grid could gain the ability to turn off wrapping
Masonry layouts could now be triggered with a value for item-flow
and more…
In this article, we want to describe Item Flow to you, the people who make websites and web apps, to see what you think. So, let’s go on a journey to imagine what this future would be like…
Combining Flexbox and Grid
Flexbox has flex-flow. It’s a shorthand for these two longhands:
Flex-direction determines which direction the content flows, while flex-wrap determines whether or not to wrap content.
CSS Grid has grid-auto-flow.
grid-auto-flow: row | column | dense;
It determines the direction in which the content flows, and whether or not dense packing is used, all in one property. It has no longhands.
So how shall we unify these capabilities? We proposed calling the new shorthand item-flow, with four new longhands:
item-direction
item-wrap
item-pack
item-slack
(It’s important to note all of these are brand new, baby fresh ideas. They will get discussed at length and changed before they become reality in browsers. To make this article readable, we are glossing over the current debates and presenting just one name for each property/value, not listing the all the variations proposed at the CSSWG.)
Item Direction
The item-direction property would determine how content flows. Should content flow in rows or columns? In the normal direction or reversed? This would work just like flex-direction and the corresponding part of grid-auto-flow.
This is pretty straightforward to understand because it works just like we expect from years of working with Flexbox and Grid. There is a debate about what “row” and “column” mean for masonry-style layouts — we’ll get into that in our next article, Item Flow – Part 2: next steps for Masonry.
Item Wrap
The item-wrap property would determine whether or not content will wrap, and if so, in which direction. The new default would be auto — which resolves to nowrap in Flexbox and wrap in Grid, matching the current initial values.
This is familiar territory, but creating a new property gives us the opportunity to clean up the values and make them more sensical. We could add a new reverse keyword, so that [ nowrap | wrap ] is one choice, while [ normal | reverse ] is a second, separate choice, improving on how this currently works in Flexbox. (We could also keep wrap-reverse around for the sake of supporting legacy habits.) This would give us:
item-wrap: auto | nowrap | wrap | normal | reverse
But CSS Grid hasn’t had a concept of nowrap before! So what would that be?!? Here’s one idea…
With Flexbox, there are many use cases for laying out your items in one line, one row, without any wrapping. Perhaps it’s a navigation bar, where each item is a phrase of a different length. The amount of space for each item is determined by the size of that item — you get flexible boxes.
What if you want to lay out all your content in one row, but, instead, for each item to get the same amount of space — for every item to be 1fr wide? Since the size of the columns is extrinsically determined, CSS Grid is the right tool for this job. But because Grid wraps by default, developers often hack Flexbox instead, not realizing they can get the desired effect today with grid-auto-flow: column.
In a future with Item Flow, it’d be very obvious how to use nowrap to tell Grid to fit everything on one line.
Now, if there are nine items, we get nine columns. If there are five items, we get five columns, like this:
Item Pack
The item-pack property would let us switch between different kinds of packing.
Dense packing
In Grid, we currently have the ability to switch to a mode of dense packing, with grid-auto-flow: dense. In the new integrated system, this same capability would be available with item-pack: normal | dense — adding an explicit value for switching back to not-dense for the first time.
Flexbox currently has no concept of dense packing. So what feature could we add to it here?
.container {
display: flex;
item-pack: dense;
}
Two ideas have been debated so far:
Dense packing could mean Flexbox will attempt to cram one more item on the row (or column) before starting a new row. It would shrink the items on the row just enough, instead of leaving extra space and growing them.
Or, to behave like Grid, dense packing could mean Flexbox will look back to previous spots in the layout and place smaller items in any available space.
To understand what these option means, let’s review how Flexbox currently works, and then take a look at what dense could do.
Today, when Flexbox starts to lay out content, fills up a row with the items that fit in the available space. When it runs out of space, it wraps (if wrapping is allowed), and fills up the next row… until all the items have a home. And then, it distributes all the extra space according to the flex and Alignment properties. (See these two steps on the left & right of the illustration.)
With the first idea listed above, dense packing in Flexbox could instead shrink items just enough to be able to fit one more item on the row, shrinking each item instead of distributing extra space.
With the second idea, to behave more like Grid dense packing, Flexbox could instead rearrange the order of items and back-fill smaller spaces with smaller items. In this case, item 6 and 10 end up out of the normal order, placed on previous rows.
This second idea would cause smaller items to pile up on the right (on the end edge). Because of this, some folks have wondered if this would be at all useful in practice.
What do you want “dense” to mean for Flexbox? Do you like the first or second idea more — or do you have a third?
Balanced packing
The item-pack property could also allow for new kinds of packing that don’t yet exist. For example, we could invent item-pack: balance.
Currently, Flexbox places items on each line without any regard to what’s happening on the other lines. This can often end up with a very few number of items on the last line.
A new balance value could tell the browser to instead balance the items in some fashion, similar to text-wrap: balance, ensuring the last line doesn’t stand out as weird.
.container {
display: flex;
item-pack: balance;
}
This could be incredibly useful and solve a long-standing frustration with Flexbox.
Collapsed packing
The item-pack property could also give us a new home for the trigger for masonry-style layouts. This is a change from the previous Masonry proposals:
Proposal
Trigger for masonry layouts
Just Use Grid
grid-template-rows: collapse
New Masonry Layout
display: masonry
Item Pack
item-pack: collapse
Going in this direction means we might not use either of the triggers for masonry layouts from Just Use Grid or New Masonry Layouts. We could abandon both in favor of this new trigger.
We’ll write more about how collapsed item packing could work for masonry layouts in our next article, Item Flow – Part 2: next steps for Masonry.
Item Slack
The item-slack property would allow you to control just how picky the layout engine is when determining where to put the next item.
For masonry-style layouts, this would be the new name for the property previously discussed as grid-slack or masonry-slack. A default slack of 1em means, “hey, when you are looking to see which column has an empty space closest to the top (as you decide where to put the next item), don’t worry about it if the difference is 1em or less. Just consider those tied.”
For Flexbox, this could set a point to switch from loose packing to cramming in extra item — which is currently set to zero. A value of 1em would mean, if it’s within 1em of fitting, cram it in, otherwise wrap to the next line. This idea could integrate with the first idea for Flexbox dense packing (described above), or replace it entirely.
Maybe -slack is not the best name. You can suggest other words in the CSSWG issue. Perhaps item-tolerance would be better, for example — or threshold, strictness, adjust, sensitivity.
The Overall Plan
Put all of these ideas together, and you get this matrix.
In case it’s not clear, web developers will be able to use the longhands or the shorthand as desired. You could, for example, define a Flexbox context with:
Of course, this whole idea for Item Flow is still a work in progress. Big changes can still be made. The names are temporary. This article’s description of how works is just the first draft. There are many conversations and many decisions to be had.
What do you think?
We would love to hear what you think about Item Flow.
Is this a good idea to combine flex-flow and grid-auto-flow into a unified system?
As a developer would you use the new syntax to accomplish the things you do today with flex-flow and grid-auto-flow?
item-wrap: auto | nowrap | wrap | normal | reverse
item-pack: normal | dense
What other ideas might you have for combining existing functionality in Flexbox and Grid into a unified system?
Are you excited about the possibilities of adding new capabilities to Grid and Flexbox? Which ones have the most potential to help you do your work, and unlock new designs?
No wrapping for Grid with: item-wrap: nowrap
Dense packing for Flexbox with: item-pack: dense
Balanced wrapping for Flexbox with: item-pack: balance
Adjusting when content is crammed/not in Flexbox with: item-slack: <length-percentage>
What other new ideas might you want to add in this unified system?
We’d love to hear what you think. Find Jen Simmons on Bluesky or Mastodon to share your feedback. Or even better, publish your thoughts in a blog post and share the link. Explore examples. Explain ideas. And let us know if you like this direction. While the CSS Working Group has resolved to pursue Item Flow, there’s still plenty of time to go in another direction if need be once we hear from web developers.
Stay tuned for Item Flow – Part 2: next steps for Masonry to learn more about how all this affects the plans for masonry layouts, and to learn more about the specific debates about how item-flow syntax could work.
Web Push notifications are a powerful and important part of the web platform.
As someone’s very famous uncle once said, with great power comes great responsibility. When we added Web Push to WebKit we knew it was imperative to maintain people’s expectations of power efficiency and privacy.
We took a deliberate approach to maintain those expectations when implementing Web Push for Safari on macOS, for web apps saved to the Home Screen on iOS and iPadOS, and web apps on Mac. We knew running extra code to display notifications could impact battery life. We knew that Web Push’s reliance on service worker JavaScript was at odds with our broad approach to user privacy on the web. We learned that the protections we felt necessary for user privacy challenged assumptions web developers had about Web Push in other browsers. So we challenged ourselves to propose something better for end users, web developers, and browsers.
Declarative Web Push allows web developers to request a Web Push subscription and display user visible notifications without requiring an installed service worker. Service worker JavaScript can optionally change the contents of an incoming notification. Unlike with original Web Push, there is no penalty for service workers failing to display a notification; the declarative push message itself is used as a fallback in case the optional JavaScript processing step fails.
Declarative Web Push is more energy efficient and more private by design. It is easier for you, the web developer to use. And it’s backwards compatible with existing Web Push notifications.
Keep reading for our thinking on the challenges that result from how Web Push works today. Or jump straight to how to use Declarative Web Push. You can test it out on iOS 18.4, iPadOS 18.4, and on the macOS 15.5 beta.
The status quo
Existing Web Push notifications were designed with a JavaScript-first mindset. Instead of a remote push directly describing a user visible notification, the more abstract concept of a “push message” is handled by the website’s service worker JavaScript.
The website first needs to have a service worker registered. It can then use that ServiceWorkerRegistration to create a PushSubscription, which gives the website the information it needs to remotely send a push message to the browser.
To give users direct control, WebKit requires you as the developer to always show a notification; no silent push messages are allowed. Therefore we require push subscriptions to set the userVisibleOnly flag to true. While this can be frustrating, the original Web Push design made this necessary to protect user privacy and battery life.
Once a push message is received on the device, the browser makes sure there is an instance of the service worker JavaScript and then dispatches a PushEvent to it. The code handling that event inspects the data in the push message, using it to make a call to ServiceWorkerRegistration.showNotification(...) to display the user visible notification.
While some popular JavaScript libraries abstract away some of these complexities, there’s a lot of code involved, and a lot can subtly go wrong.
Challenge 1 — Silent push protection
Recall that WebKit requires the userVisibleOnly flag be set to true when registering for a push subscription. The JavaScript in a ServiceWorker’s PushEvent handler has the responsibility of showing that user visible notification. Allowing websites to remotely wake up a device for silent background work is a privacy violation and expends energy. So if an event handler doesn’t show the user visible notification for any reason we revoke its push subscription
Unfortunately bugs in a service worker script, networking conditions, or local device conditions all might prevent a timely call to showNotification. These scenarios might not always be the fault of the script author and can be difficult to debug. It would be better if there were technical enforcement of the userVisibleOnly promise and therefore the silent push penalty box could be ignored.
Challenge 2 — Tracking data
We’ve blogged about it before and we’ll blog about it again; Privacy is a fundamental human right.
Since the first version of Safari we’ve focused on privacy. WebKit goes above and beyond the privacy protections required by web standards. As the web platform evolved, so did our strategies to protect user privacy. This now includes active blocking and removal of website data, like with Intelligent Tracking Prevention (shortened as ITP).
ITP deletes all website data for websites you haven’t visited in a while. This includes service worker registrations. While this can be frustrating to web developers, it’s key to protecting user privacy. It’s a hard tradeoff we make intentionally given how committed we are to protecting users.
When we implemented Web Push that created a dilemma. Since creating and using a push subscription is inherently tied to having a service worker, ITP removing a service worker registration would render the push subscription useless. Since having strong anti-tracking prevention features seems to be fundamentally at odds with the JavaScript-driven nature of existing Web Push, wouldn’t it be better if Web Push notifications could be delivered without any JavaScript?
So what does Declarative Web Push look like in practice?
How to use Declarative Web Push
To use any flavor of Web Push you first use a PushManager to acquire a push subscription. Web Push on Apple’s platforms uses the same Apple Push Notification service that powers native push on all Apple devices. You do not need to be a member of the Apple Developer Program to use it.
The only PushManager available with original Web Push is ServiceWorkerRegistration.pushManager.
Declarative Web Push also exposes window.pushManager to support subscription management without requiring a service worker.
If you do also have a registered service worker scoped to the root level of your website domain, it shares the same push subscription as the window object. But the removal of that service worker registration will not affect the associated push subscription.
Sending a push message to that push subscription works exactly as before. For a notification to be handled declaratively the contents of the push message must match the declarative standard JSON format. This standardized format guarantees that the browser has enough information to display a user-visible notification without any JavaScript.
{
"web_push":8030,
"notification": {
"title":"Webkit.org — Meet Declarative Web Push",
"lang":"en-US",
"dir":"ltr",
"body":"Send push notifications without JavaScript or service worker!",
"navigate":"https://webkit.org/blog/16535/meet-declarative-web-push/",
"silent":false,
"app_badge":"1"
}
}
The top level "web_push" value is an homage to RFC 8030 – Generic Event Delivery Using HTTP Push. This is the magic value that opts the rest of your push message into declarative parsing.
The "notification" value is a dictionary that describes the user visible notification to the browser. Like when you create a notification programmatically in JavaScript, a non-empty "title" value is required. Most of the optional members of the NotificationOptions dictionary can also be specified.
So far we’ve mostly discussed the automatic display of a notification without JavaScript. Something useful needs to happen without JavaScript when a user activates a declarative notification. That’s where the required "navigate" value comes in. It describes a URL that will be navigated to by the browser upon activation.
Finally, if the web app supports running in an app-like mode that supports the Badging API, such as Home Screen web apps on iOS, the declarative message can include an updated application badge.
A note on backwards compatibility
In practice, a vast majority of Web Push messages are already JSON. They describe a user visible notification to be displayed. The service worker JavaScript handling those push messages simply parses the JSON to display the notification programatically. But the format of those JSON messages varies on a per-website basis.
Most applications will find it straightforward to send the declarative standard JSON in their push messages and rewrite their service worker’s PushEvent handler to display it. Once those two steps are taken, those Web Push messages become backwards compatible with browsers that do not yet support Declarative Web Push.
If your push message arrives to a newer browser, it’s handled declaratively by the browser. If it arrives to an older browser, it’s handled imperatively by JavaScript as it always had been.
Always standardizing on the declarative standard JSON has the nice side effect of introducing consistency across all projects, further reducing maintenance burden for prolific web developers.
What if I can’t send the notification description through the internet?
All apps — no matter their platform — might not be able to send the visible content of a notification through push services. How a notification should display often relies on application state local to the user’s device. Maybe the user has used the app in ways the server is not aware of yet, requiring an update to the notification. Or maybe the app is for secure communication and decryption keys for the notification payload only exist within the app on the device.
In these cases, code is needed to process the incoming push message to display something meaningful.
Native iOS apps that run into these edge cases have a tool call UNNotificationServiceExtension which allows a small snippet of application code to run in response to an incoming push notification. The incoming notification always has enough content to display a user visible “fallback notification”, but the app’s notification service extension is given a short amount of time to consult the app’s local data storage and propose a new, more meaningful notification.
If there’s a bug in the notification extension, or the required data is not available, or some other unforeseen scenario causes it to fail to show a different notification in time, the original “fallback content” is displayed instead.
For web apps with Declarative Web Push, service worker JavaScript fills the same role. When a Declarative Web Push message arrives and a service worker is installed, a push event is dispatched to it like before.
PushEvent now has the context of the “proposed notification” from the Declarative Web Push message. If the event handler displays a replacement notification properly, the proposed notification is ignored. If the event handler fails to display a replacement notification in time, the fallback is used.
Because there is always a user visible notification — therefore a privacy breaking silent push remains impossible — browsers don’t have to apply their “silent push penalties” to Declarative Web Push messages.
iOS also supports offloading unused native apps which frees up the storage used by the application code while leaving minimal application functionality in place. In this scenario, the UNNotificationServiceExtension code is gone but unmodified notifications can still be displayed for the application.
This is quite similar to how Declarative Web Push notifications work on iOS after the website’s service worker JavaScript has been removed either by the user or by ITP. The modification of an incoming push message is no longer possible but the unmodified notification can still be displayed.
Standards work
We’re excited about Declarative Web Push and want it supported everywhere.
Around the time we published our WebKit Declarative Web Push explainer, we also approached the other browser vendors and interested parties about it at TPAC 2023 and raised an issue against the Push API to discuss it. While standards bodies will always nit pick on the details, the overall goal of the proposal was well met.
In 2024, we actively made proposals to the various specs involved, making changes to our implementation as well reasoned feedback came in:
Allow notifications and actions to specify a navigable URL against the Notifications API enables notifications to navigate without involving JavaScript after an end user clicks them. (We also made various editorial changes to the Notifications API as part of standardizing Declarative Web Push.)
Expose pushManager on Window against the Push API specification enables the Push API API to be used without the need for a service worker.
We feel the feature has a good enough foundation for us to ship it and for web developers to start experimenting with it. We foresee future enhancements enabled by this solid foundation. And we hope it ends up widely available soon.
We encourage you to reach out to us on WebKit’s Slack or our issue tracker to share your experiences working with this great new feature.
Update on what happened in WebKit in the week from March 17 to March 24.
Cross-Port 🐱
Limited the amount data stored for certain elements of WebKitWebViewSessionState. This results in memory savings, and avoids oddly large objects which resulted in web view state being restored slowly.
Multimedia 🎥
GStreamer-based multimedia support for WebKit, including (but not limited to) playback, capture, WebAudio, WebCodecs, and WebRTC.
The built-in JavaScript/ECMAScript engine for WebKit, also known as JSC or SquirrelFish.
Fixed an integer overflow when using wasm/gc on 32-bits.
Graphics 🖼️
Landed a change that fixes a few scenarios where the damage was not generated on layer property changes.
Releases 📦️
WebKitGTK 2.48.0 and WPE WebKit 2.48.0 have been released. While they may not look as exciting as the 2.46 series, which introduced the use of Skia for painting, they nevertheless includes half a year of improvements. This development cycle focused on reworking internals, which brings modest performance improvements for all kinds of devices, but most importantly cleanups which will enable further improvements going forward.
For those who need longer to integrate newer releases, which we know can be a longer process for embedded device distrihytos, we have also published WPE WebKit 2.46.7 with a few stability and security fixes.
Accompanying these releases there is security advisory WSA-2025-0002 (GTK, WPE), which covers the solved security issues. Crucially, all three contain the fix for an issue known to be exploited in the wild, and therefore we strongly encourage updating.
Update on what happened in WebKit in the week from March 10 to March 17.
Cross-Port 🐱
Web Platform 🌐
Updated button activation behaviour and type property reflection with command and commandfor. Also aligned popovertarget behaviour with latest specification.
Safari Technology Preview Release 215 is now available for download for macOS Sequoia and macOS Sonoma. If you already have Safari Technology Preview installed, you can update it in System Settings under General → Software Update.
Added support for Scroll Driven Animations. (290471@main) (144887859)
Added support for text-wrap-style: pretty. (291092@main) (145577976)
Added support for CSS Anchor Positioning. (291214@main) (145681750)
JavaScript
Resolved Issues
Fixed processing of an alternation of strings (290982@main) (145222010)
Networking
Resolved Issues
Fixed using WebSocket in a WebWorker causing the entire Worker to freeze. (290802@main) (145149784)
Scrolling
Resolved Issues
Fixed autoscrolling for smooth scrolling while selecting text. (290497@main) (144900491)
Text
Resolved Issues
Fixed generating scroll to text fragments around text that contains newlines. (290761@main) (137109344)
Fixed generating text fragments when the selected text starts and ends in different blocks. (290748@main) (137761701)
Fixed Copy Link to Highlight not working when selecting text that is its own block and when that text exists higher up in the document. (290683@main) (144392379)
Fixed selections that start or end in white space not creating text fragments. (291146@main) (145614181)
Web API
New Features
Added support for Trusted Types. (291409@main) (130065736)
Added support for the File System WritableStream API. (291399@main) (145875384)
Resolved Issues
Fixed fullscreen to use a single queue for event dispatching. (290898@main) (145372389)
Web Extensions
Resolved Issues
Fixed a bug where the runtime.MessageSender origin parameter would be lowercased, differing from the result returned from runtime.getURL. (291118@main) (140291738)
Update on what happened in WebKit in the week from March 3 to March 10.
Cross-Port 🐱
Web Platform 🌐
Forced styling to field-sizing: fixed when an input element is auto filled, and added
support for changing field-sizing
dynamically.
Fixed an issue where the imperative
popover APIs didn't take into account the source parameter for focus behavior.
Multimedia 🎥
GStreamer-based multimedia support for WebKit, including (but not limited to) playback, capture, WebAudio, WebCodecs, and WebRTC.
Fixed YouTube breakage on videos with
advertisements. The fix prevents scrolling to the comments section when the
videos are fullscreened, but having working video playback was considered more
important for now.
Graphics 🖼️
Fixed re-layout issues for form
controls with the experimental field-sizing implementation.
Landed a change that improves
the quality of damage rectangles and reduces the amount of painting done in the
compositor in some simple scenarios.
Introduce a hybrid threaded rendering
mode, scheduling tasks to both the CPU and GPU worker pools. By default we use
CPU-affine rendering on WPE, and GPU-affine rendering on the GTK port,
saturating the CPU/GPU worker pool first, before switching to the GPU/CPU.
Infrastructure 🏗️
We have recently enabled automatic nightly runs of WPT tests with WPE for the
Web Platform Tests (WPT) dashboard. If you click on the “Edit” button at the
wpt.fyi dashboard now there is the option to select WPE.
These nightly runs happen now daily on the TaskCluster CI sponsored by Mozilla
(Thanks to James Graham!).
If you want to run WPT tests with WPE WebKit locally, there are instructions
at the WPT documentation.
After fixing
an issue with Trusted Types when doing attribute mutation within the default
callback, and implementing
performance improvements for Trusted Types enforcement, the
Trusted Types
implementation is now considered stable and has been
enabled by default.
Multimedia 🎥
GStreamer-based multimedia support for WebKit, including (but not limited to) playback, capture, WebAudio, WebCodecs, and WebRTC.
Landed one fix which,
along with previous patches, solved the webKitMediaSrcStreamFlush() crash
reported in bug #260455.
Unfortunately, in some pages where the crash previously occurred, now a
different blank video bug has been revealed. The cause of this bug is known,
but fixing it would cause performance regressions in pages with many video
elements. Work is ongoing to find a better solution for both.
The initial support of MP4-muxed WebVTT in-band text
tracks is about to be merged,
which will bring this MSE feature to the ports using GStreamer. Text tracks
for the macOS port of WebKit only landed two weeks
ago and we expect there will be
issues to iron out in WebKit ports, multiplatform code and even potentially in
spec work—we are already aware of a few potential ones.
Note that out-of band text-tracks are well supported in MSE across browsers
and commonly used. On the other hand, no browsers currently ship with in-band
text track support in MSE at this point.
Support for MediaStreamTrack.configurationchange events was
added,
along with related
improvements
in the GStreamer PipeWire plugin. This will allow WebRTC applications to
seamlessly handle default audio/video capture changes.
Graphics 🖼️
Continued improving the support for handling graphics damage:
Added support
for validating damage rectangles in Layout Tests.
Landed a change that adds
layout tests covering the damage propagation feature.
Landed a change that fixes
damage rectangles on layer resize operations.
Landed a change that
improves damage rectangles produced by scrolling so that they are
clipped to the parent container.
The number of threads used for painting with the GPU has been slightly
tweaked, which brings a measurable
performance improvement in all kinds of devices with four or mores processor
cores.
Releases 📦️
The stable branch
for the upcoming 2.48.x stable release series of the GTK and WPE ports has
been created. The first preview releases from this branch are WebKitGTK
2.47.90 and
WPE WebKit 2.47.90.
People willing to report issues and help with stabilization are encouraged to
test them and report issues in Bugzilla.
Community & Events 🤝
Published a blog
post
that presents an opinionated approach to the work with textual logs obtained
from WebKit and GStreamer.
Safari Technology Preview Release 214 is now available for download for macOS Sequoia and macOS Sonoma. If you already have Safari Technology Preview installed, you can update it in System Settings under General → Software Update.
WebKit has grown into a massive codebase throughout the years. To make developers’ lives easier, it offers various subsystems and integrations.
One such subsystem is a logging subsystem that offers the recording of textual logs describing an execution of the internal engine parts.
The logging subsystem in WebKit (as in any computer system), is usually used for both debugging and educational purposes. As WebKit is a widely-used piece of software that runs on
everything ranging from desktop-class devices up to low-end embedded devices, it’s not uncommon that logging is sometimes the only way for debugging when various limiting
factors come into play. Such limiting factors don’t have to be only technical - it may also be that the software runs on some restricted systems and direct debugging is not allowed.
Requirements for efficient work with textual logs #
Regardless of the reasons why logging is used, once the set of logs is produced, one can work with it according to the particular need.
From my experience, efficient work with textual logs requires a tool with the following capabilities:
Ability to search for a particular substring or regular expression.
Ability to filter text lines according to the substring or regular expressions.
Ability to highlight particular substrings.
Ability to mark certain lines for separate examination (with extra notes if possible).
Ability to save and restore the current state of work.
While all text editors should be able to provide requirement 1, requirements 2-5 are usually more tricky and text editors won’t support them out of the box.
Fortunately, any modern extensible text editor should be able to support requirements 2-5 after some extra configuration.
Throughout the following sections, I use Emacs, the classic “extensible, customizable, free/libre text editor”, to showcase how it can be set up and used to meet
the above criteria and to make work with logs a gentle experience.
Emacs, just like any other text editor, provides the support for requirement 1 from the previous section out of the box.
To support requirement 2, it requires some extra mode. My recommendation for that is loccur - the minor mode
that acts just like a classic grep *nix utility yet directly in the editor. The benefit of that mode (over e.g. occur)
is that it works in-place. Therefore it’s very ergonomic and - as I’ll show later - it works well in conjunction with bookmarking mode.
Installation of loccur is very simple and can be done from within the built-in package manager:
M-x package-install RET loccur RET
With loccur installed, one can immediately start using it by calling M-x loccur RET <regex> RET. The figure below depicts the example of filtering:
highlight-symbol - the package with utility functions for text highlighting #
To support requirement 3, Emacs also requires the installation of extra module. In that case my recommendation is highlight-symbol
that is a simple set of functions that enables basic text fragment highlighting on the fly.
Installation of this module is also very simple and boils down to:
M-x package-install RET highlight-symbol RET
With the above, it’s very easy to get results like in the figure below:
just by moving the cursor around and using C-c h to toggle the highlight of the text at the current cursor position.
bm - the package with utility functions for buffer lines bookmarking #
Finally, to support requirements 4 and 5, Emacs requires one last extra package. This time my recommendation is bm
that is quite a powerful set of utilities for text bookmarking.
In this case, installation is also very simple and is all about:
M-x package-install RET bm RET
In a nutshell, the bm package brings some visual capabilities like in the figure below:
as well as non-visual capabilities that will be discussed in further sections.
Once all the necessary modules are installed, it’s worth to spend some time on configuration. With just a few simple tweaks it’s possible to make the work with logs
simple and easily reproducible.
To not influence other workflows, I recommend attaching as much configuration as possible to any major mode and setting that mode as a default for
files with certain extensions. The configuration below uses a major mode called text-mode as the one for working with logs and associates all the files with a
suffix .log with it. Moreover, the most critical commands of the modes installed in the previous sections are binded to the key shortcuts. The one last
thing is to enable truncating the lines ((set-default 'truncate-lines t)) and highlighting the line that the cursor is currently at ((hl-line-mode 1)).
To show what the workflow of Emacs is with the above configuration and modules, some logs are required first. It’s very easy to
get some logs out of WebKit, so I’ll additionally get some GStreamer logs as well. For that, I’ll build a WebKit GTK port from the latest revision of WebKit repository.
To make the build process easier, I’ll use the WebKit container SDK.
The above command disables the ENABLE_JOURNALD_LOG build option so that logs are printed to stderr. This will result in the WebKit and GStreamer logs being bundled together as intended.
Once the build is ready, one can run any URL to get the logs. I’ve chosen a YouTube conformance tests suite from 2021 and selected test case “39. PlaybackRateChange”
to get some interesting entries from multimedia-related subsystems:
Once the logs are collected, one can open them using Emacs and start making sense out of them by gradually exploring the flow of execution. In the below exercise, I intend to understand
what happened from the multimedia perspective during the execution of the test case “39. PlaybackRateChange”.
The first step is usually to find the most critical lines that mark more/less the area in the file where the interesting things happen. In that case I propose using M-x loccur RET CONSOLE LOG RET to check what the
console logs printed by the application itself are. Once some lines are filtered, one can use bm-toggle command (C-c t) to mark some lines for later examination (highlighted as orange):
For practicing purposes I propose exiting the filtered view M-x loccur RET and trying again to see what events the browser was dispatching e.g. using M-x loccur RET on node node 0x7535d70700b0 VIDEO RET:
In general, the combination of loccur and substring/regexp searches should be very convenient to quickly explore various types of logs along with marking them for later. In case of very important log
lines, one can additionally use bm-bookmark-annotate command to add extra notes for later.
Once some interesting log lines are marked, the most basic thing to do is to jump between them using bm-previous (C-c n) and bm-next (C-c p). However, the true power of bm mode comes with
the use of M-x bm-show RET to get the view containing only the lines marked with bm-toggle (originally highlighted orange):
This view is especially useful as it shows only the lines deliberately marked using bm-toggle and allows one to quickly jump to them in the original file. Moreover, the lines are displayed in
the order they appear in the original file. Therefore it’s very easy to see the unified flow of the system and start making sense out of the data presented. What’s even more interesting,
the view contains also the line numbers from the original file as well as manually added annotations if any. The line numbers are especially useful as they may be used for resuming the work
after ending the Emacs session - which I’ll describe further in this section.
When the *bm-bookmarks* view is rendered, the only problem left is that the lines are hard to read as they are displayed using a single color. To overcome that problem one can use the macros from
the highlight-symbol package using the C-c h shortcut defined in the configuration. The result of highlighting some strings is depicted in the figure below:
With some colors added, it’s much easier to read the logs and focus on essential parts.
On some rare occasions it may happen that it’s necessary to close the Emacs session yet the work with certain log file is not done and needs to be resumed later. For that, the simple trick is to open the current
set of bookmarks with M-x bm-show RET and then save that buffer to the file. Personally, I just create a file with the same name as log file yet with .bm prefix - so for log.log it’s log.log.bm.
Once the session is resumed, it is enough to open both log.log and log.log.bm files side by side and create a simple ad-hoc macro to use line numbers from log.log.bm to mark them again in the log.log
file:
As shown in the above gif, within a few seconds all the marks are applied in the buffer with log.log file and the work can resume from that point i.e. one can jump around using bm, add new marks etc.
Although the above approach may not be ideal for everybody, I find it fairly ergonomic, smooth, and covering all the requirements I identified earlier.
I’m certain that editors other than Emacs can be set up to allow the same or very similar flow, yet any particular configurations are left for the reader to explore.
Igalia is arranging the twelfth annual Web Engines Hackfest, which will be held on Monday 2nd June through Wednesday 4th June.
As usual, this is a hybrid event, at Palexco in A Coruña (Galicia, Spain) as well as remotely.
Registration is now open:
Submit your talks and breakout sessions. The deadline to submit proposals is Wednesday 30th April.
The Web Engines Hackfest is an event where folks working on various parts of the web platform gather for a few days to share knowledge and discuss a variety of topics.
These topics include web standards, browser engines, JavaScript engines, and all the related technology around them.
Last year, we had eight talks (watch them on YouTube) and 15 breakout sessions (read them on GitHub).
A wide range of experts with a diverse set of profiles and skills attend each year, so if you are working on the web platform, this event is a great opportunity to chat with people that are both developing the standards and working on the implementations.
We’re really grateful for all the people that regularly join us for this event; you are the ones that make this event something useful and interesting for everyone! 🙏
Really enjoying Web Engines Hackfest by @igalia once again. Recommended for everyone interested in web technology.
The breakout sessions are probably the most interesting part of the event.
Many different topics are discussed there, from high-level issues like how to build a new feature for the web platform, to lower-level efforts like the new WebKit SDK container.
Together with the hallway discussions and impromptu meetings, the Web Engines Hackfest is an invaluable experience.
Big shout-out to Igalia for organising the Web Engines Hackfest every year since 2014, as well as the original WebKitGTK+ Hackfest starting in 2009.
The event has grown and we’re now close to 100 onsite participants with representation from all major browser vendors.
If your organization is interested in helping make this event possible, please contact us regarding our sponsorship options.
The GStreamer video frame converter, used to show video frames in the canvas or
WebGL, has been fixed to use the right GL context and now supports DMA-BUF
video frames, too.
Added support to the eotf
additional MIME type parameter when checking for supported multimedia content
types. This is required by some streaming sites like YouTube TV.
The WebKit ports that use CMake as their build system—as is the case for the GTK and WPE ones—now enable C++ library assertions by default, when building against GNU libstdc++ or LLVM's libc++. This adds lightweight runtime checks to a number of C++ library facilities, mostly aimed at detecting out-of-bounds memory access, and does not have a measurable performance impact on typical browsing workloads.
A number of Linux distributions were already enabling these assertions as part of their security hardening efforts (e.g. Fedora or Gentoo) and they do help finding actual bugs. As a matter of fact, a number of issues were fixed before all the WebKit API and layout tests with the assertions enabled and the patch could be merged! Going forward, this will prevent accidentally introducing bugs new due to wrong usage of the C++ standard library.
Update on what happened in WebKit in the week from February 3 to February 11.
Cross-Port 🐱
Fixed an assertion crash in the
remote Web Inspector when its resources contain an UTF-8 “non-breaking space”
character.
Multimedia 🎥
GStreamer-based multimedia support for WebKit, including (but not limited to) playback, capture, WebAudio, WebCodecs, and WebRTC.
Media playback now supports choosing the output audio device on a per element basis, using the setSinkId() API. This also added the support needed for enumerating audio outputs, which is needed by Web applications to obtain the identifiers of the available devices. Typical usage includes allowing the user to choose the audio output used in WebRTC-based conference calls.
For now feature flags ExposeSpeakers, ExposeSpeakersWithoutMicrophone, and PerElementSpeakerSelection need to be enabled for testing.
Landed a change that adds a
visualization for damage rectangles, controlled by the WEBKIT_SHOW_DAMAGE
environment variable. This highlights areas damaged during rendering of every
frame—as long as damage propagation is enabled.
Releases 📦️
Stable releases of WebKitGTK 2.46.6 and WPE WebKit 2.46.6 are now available. These come along with the first security advisory of the year (WSA-2025-0001: GTK, WPE): they contain mainly security fixes, and everybody is advised to update.
The unstable release train continues as well, with WebKitGTK 2.47.4 and WPE WebKit 2.47.4 available for testing. These are snapshots of the current development status, and while expected to work there may be rough edges—if you encounter any issue, reports at the WebKit Bugzilla are always welcome.
The recently released libwpe 1.16.1 accidentally introduced an ABI break, which has been corrected in libwpe 1.16.2. There are no other changes, and the latter should be preferred.
This blog post is to announce that Igalia has gotten a grant from NLnet Foundation to work on solving cross-root ARIA issues in Shadow DOM. My colleague Alice Boxhall, which has been working on sorting out this issue since several years ago, together with support form other igalians is doing the work related to this grant.
Briefly speaking, there are mainly two different problems when you want to reference elements for ARIA attributes cross shadow root boundaries.
First issue is that you cannot reference things outside the Shadow DOM. Imagine you have a custom element (#customButton) which contains a native button in its Shadow DOM, and you want to associate the internal button with a label (#label) which is outside in the light DOM.
And the second problem is that you cannot reference things inside a Shadow DOM from the outside. Imagine the opposite situation where you have a custom label (#customLabel) with a native label in its Shadow DOM that you want to reference from a button (#button) in the light DOM.
This is a huge issue for web components because they cannot use Shadow DOM, as they would like due to its encapsulation properties, if they want to provide an accessible experience to users. For that reason many of the web components libraries don’t use yet Shadow DOM and have to rely on workarounds or custom polyfills.
The Accessibility Object Model (AOM) effort was started several years ago aiming to solve several issues including the one described before, that had a wider scope and tried to solve many different things including the problems described in this blog post. At that time Alice was at Google and Alex Surkov at Mozilla, both were part of this effort. Coincidentally, they are now at Igalia, which together with Joanmarie Diggs and Valerie Young create a dream team of accessibility experts in our company.
Even when the full problem hasn’t been sorted out yet, there has been some progress with the Element Reflection feature which allows ARIA relationship attributes to be reflected as element references. Whit this users can specify them without the need to assign globally unique ID attributes to each element. This feature has been implemented in Chromium and WebKit by Igalia. So instead of doing something like:
<buttonid="button"aria-describedby="description">Button</button> <divid="description">Description of the button.</div>
You could specify it like:
<buttonid="button">Button</button> <divid="description">Description of the button.</div> <script> button.ariaDescribedByElements =[description]; </script>
Coming back to Shadow DOM, this feature also enables authors to specify ARIA relationships pointing to things outside the Shadow DOM (the first kind of problem described in the previous section), however it doesn’t allow to reference elements inside another Shadow DOM (the second problem). Anyway let’s see an example of how this will solve the first issue:
const button = document.createElement("button"); button.textContent ="Button"; /* Here is where we reference the outer label from the button inside the Shadow DOM. */ button.ariaLabelledByElements =[label]; shadowRoot.appendChild(button);
const bar = document.createElement("div"); bar.textContent ="bar"; shadowRoot.appendChild(bar); </script>
Apart from Element Reflection, which only solves part of the issues, there have been other ideas about how to solve these problems. Initially Cross-root ARIA Delegation proposal by Leo Balter at Salesforce. A different one called Cross-root ARIA Reflection by Westbrook Johnson at Adobe. And finally the Reference Target for Cross-root ARIA proposal by Ben Howell at Microsoft.
Again if you want to learn more about the different nuances of the previous proposals you can revisit Alice’s blog post.
At this point this is the most promising proposal is the Reference Target one. This proposal allows the web authors to use Shadow DOM and still don’t break the accessibility of their web applications. The proposal is still in flux and it’s currently being prototyped in Chromium and WebKit. Anyway as an example this is the kind of API shape that would solve the second problem described in the initial section, where we reference a label (#actualLabel) inside the Shadow DOM from a button (#button) in the light DOM.
As part of this grant we’ll work on all the process to get the Reference Target proposal ready to be shipped in the web rendering engines. Some of the tasks that will be done during this project include work in different fronts:
Proposal: Help with the work on the proposal identifying issues, missing bits, design solutions, providing improvements, keeping it up to date as the project evolves.
Specification: Write the specification text, discuss and review it with the appropriate working groups, improved it based on gathered feedback and implementation experience, resolve issues identified in the standards bodies.
Implementation: Prototype implementation in WebKit to verify the proposal, upstream changes to WebKit, fix bugs on the implementation, adapt it to spec modifications.
Testing: Analyze current WPT tests, add new ones to increase coverage, validate implementations, keep them up-to-date as things evolve.
Outreach: Blog posts explaining the evolution of the project and participation in events with other standards folks to have the proper discussions to move the proposal and specification forward.
We’re really grateful that NLnet has trusted us to this project, and we really hope this will allow to fix an outstanding accessibility issue in the web platform that has been around for too many time already. At the same point it’s a bit sad, that the European Union through the NGI funds is the one sponsoring this project, when it will have a very important impact for several big fishes that are part of the Web Components WG.
If you want to follow the evolution of this project, I’d suggest you to follow Alice’s blog where she’ll be updating us about the progress of the different tasks.
Update on what happened in WebKit in the week from January 27 to February 3.
Cross-Port 🐱
The documentation now has a section on how to use the Web Inspector
remotely. This makes information on
this topic easier to find, as it was previously scattered around a few
different locations.
Jamie continues her Coding Experience work around bringing WebExtensions to the
WebKitGTK port. A good part of this involves porting
functionality
from Objective-C, which only the Apple WebKit ports would use, into C++ code
that all ports may use. The latest in this saga was
WebExtensionStorageSQLiteStore.
Web Platform 🌐
The experimental support for Invoker
Commands
has been updated to match latest
spec changes.
GStreamer-based multimedia support for WebKit, including (but not limited to)
playback, capture, WebAudio, WebCodecs, and WebRTC.
The GStreamer WebRTC backend can now recycle inactive
senders
and support for inactive receivers was also improved. With these changes,
support for screen sharing over WebRTC is now more reliable.
Nushell is a new shell (get it?) in development since 2019. Where other shells like bash and zsh treat all data as raw text,
nu instead provides a type system for all data flowing through its pipelines, with many commands inspired by functional languages to manipulate
that data. The examples on their homepage and in the README.md demonstrate this well,
and I recommend taking a quick look if you’re not familiar with the language.
I have been getting familiar with Nu for a few months now, and found it a lot more approachable and user-friendly than traditional shells,
and particularly helpful for exploring logs.
I won’t go over all the commands I use in detail, so if anything is ever unclear, have a look at the Command Reference.
The most relevant categories for our use case are probably Strings and Filters.
From inside nushell, you can also use help some_cmd or some_cmd --help, or help commands for a full table of commands that can be manipulated and searched like
any other table in nu. And for debugging a pipeline, describe is a very useful command that describes the type of its input.
First of all, we need some custom commands to parse the raw logs into a nu table. Luckily, nushell provides a parse command for exacly this use case,
and we can define this regex to use with it:
(I use a simple pipeline here to split the string over multiple lines for better readability, it just concatenates the list elements.)
Lets run a simple pipeline to get some logs to play around with: GST_DEBUG=*:DEBUG GST_DEBUG_FILE=sample.log gst-launch-1.0 videotestsrc ! videoconvert ! autovideosink
For parsing the file, we need to be careful to remove any ansi escapes, and split the input into lines. On top of that, we will also store the result to a variable for ease of use: let gst_log = open sample.log | ansi strip | lines | parse --regex $gst_regex
You can also define a custom command for this, which would look something like:
Define it directly on the command line, or place it in your configuration files. Either way, use the command like this: let gst_log = open sample.log | from gst logs
Some basic commands for working with the parsed data #
If you take a look at a few lines of the table, it should look something like this: $gst_log | skip 10 | take 10
skip and take do exactly what it says on the tin - removing the first N rows, and showing only the first N rows, respectively. I use them here to keep the examples short.
To ignore columns, use reject: $gst_log | skip 10 | take 5 | reject time pid thread
Meanwhile, get returns a single column as a list, which can for example be used with uniq to get a list of all objects in the log: $gst_log | get object | uniq | take 5
Filtering rows by different criteria works really well with where. $gst_log | where thread in ['0x7f467c000b90' '0x232fefa0'] and category == GST_STATES | take 5
And if you need to merge multiple logs, I recommend using sort-by time. This could look like let gst_log = (open sample.log) + (open other.log) | from gst logs | sort-by time
While there are many other useful commands, there is one more command I find incredbly useful: explore.
It is essentially the nushell equivalent to less, and while it is still quite rough around the edges,
I’ve been using it all the time, mostly for its interactive REPL.
First, just pipe the parsed log into explore: $gst_log | explore
Now, using the :try command opens its REPL. Enter any pipeline at the top, and you will be able to explore its output below:
Switch between the command line and the pager using Tab, and while focused on the pager, search forwards or backwards using / and ?, or enter :help for explanations.
Also have a look at the documentation on explore in the Nushell Book.
GStreamer-based multimedia support for WebKit, including (but not limited to) playback, capture, WebAudio, WebCodecs, and WebRTC.
Fixed the assertion error "pipeline and player states are not synchronized" related to muted video playback in the presence of scroll. Work is ongoing regarding other bugs reproduced with the same video, some of them related to scroll and some likely indepedent.
Fixed lost initial audio
samples played using WebAudio on
32-bit Raspberry Pi devices, by preventing the OpenMAX subsystem to enter
standby mode.
Graphics 🖼️
Landed a change that fixes damage propagation of 3D-transformed layers.
Fixed a regression visiting any web page making use of accelerated ImageBuffers (e.g. canvas) when CPU rendering is used. We were unconditionally creating OpenGL fences, even in CPU rendering mode, and tried to wait for completion in a worker thread, that had no OpenGL context (due to CPU rendering). This is an illegal operation in EGL and fired an assertion, crashing the WebProcess.
Releases 📦️
Despite the work on the WPE Platform API, we continue to maintain the “classic” stack based on libwpe. Thus, we have released libwpe 1.16.1 with the small—but important—addition of support for representing analog button inputs for devices capable of reporting varying amounts of pressure.
Update on what happened in WebKit in the week from January 13 to January 20.
Cross-Port 🐱
JavaScriptCore 🐟
The built-in JavaScript/ECMAScript engine for WebKit, also known as JSC or SquirrelFish.
The JavaScriptCore GLib API has gained
support
for creating
Promise
objects. This allows integrating asynchronous functionality more ergonomically when
interfacing between native code and JavaScript.
Landed a change that adds
multiple fixes to the damage propagation functionality in scenarios such as:
Layers with custom transforms.
Pages with custom viewport scale.
Dynamic layer size changes.
Scrollbar layers.
Landed a change that improves
damage propagation in terms of animations handling.
Landed a change that prevents
any kind of damage propagation when the feature is disabled at runtime using
its corresponding flag. Before that, even though the functionality was
runtime-disabled some memory usage and unneeded calculations were being done.
WPE WebKit 📟
WPE Platform API 🧩
New, modern platform API that supersedes usage of libwpe and WPE backends.
Drag gesture threshold, and key repeat delay/interval are now handled through
the WPESettings API instead of using
hardcoded values. While defaults typically work well, being able to tweak them
for certain setups without rebuilding WPE is a welcome addition.
Sylvia has also improved the WPE Platform DRM/KMS backend to pick the
default output device scaling factor
using WPESettings.
Infrastructure 🏗️
libsoup has been added to
Google's OSS-Fuzz program to help find
security issues.
Update on what happened in WebKit in the week from December 31, 2024 to January 13, 2025.
Cross-Port 🐱
Web Platform 🌐
Landed a fix to the experimental Trusted Types implementation for certain event handler content attributes not being protected even though they are sinks.
Landed a
fix
to experimental Trusted Types implementation where the
SVGScriptElement.className property was being protected even though it's not
a sink.
Multimedia 🎥
GStreamer-based multimedia support for WebKit, including (but not limited to) playback, capture, WebAudio, WebCodecs, and WebRTC.
Support for the H.264 “constrained-high” and “high” profiles was
improved in the GStreamer WebRTC
backend.
The GStreamer WebRTC backend now has basic support for network conditions
simulation, that will be useful to
improve error recovery and packet loss coping mechanisms.
JavaScriptCore 🐟
The built-in JavaScript/ECMAScript engine for WebKit, also known as JSC or SquirrelFish.
JSC got a fix
for a tricky garbage-collection issue.
Graphics 🖼️
Landed a
change
that enables testing the "damage propagation" functionality. This
is a first step in a series of fixes and improvements that should stabilize
that feature.
Damage propagation passes extra information that describes the viewport areas
that have visually changed since the last frame across different graphics
subsystems. This allows the WebKit compositor and the system compositor to
reduce the amount of painting being done thus reducing usage of resources
(CPU, GPU, and memory bus). This is especially helpful on constrained,
embedded platforms.
WebKitGTK 🖥️
A patch landed to add metadata
(title and creation/modification date) to PDF documents generated for
printing.
The “suspended” toplevel state is now handled in GTK
port to pause rendering when web
views are fully obscured.
Jamie Murphy is doing a Coding Experience focused on adding support for WebExtensions. After porting a number of Objective-C classes to C++, to allow using them in all WebKit ports, she has recently made the code build on Linux, and started addingnew public API to expose the functionality to GTK applications that embed web views. There is still plenty of work to do, but this is great progress nevertheless.
Update on what happened in WebKit in the week from December 23 to December 30.
Community & Events 🤝
Published an article on CSS Anchor Positioning. It discusses the current status of the support across browsers, Igalia's contributions to the WebKit's implementation, and the predictions for the future.
CSS Anchor Positioning is a novel CSS specification module
that allows positioned elements to size and position themselves relative to one or more anchor elements anywhere on the web page.
In simpler terms, it is a new web platform API that simplifies advanced relative-positioning scenarios such as tooltips, menus, popups, etc.
To better understand the true power it brings, let’s consider a non-trivial layout presented in Figure 1:
In the past, creating a context menu with position: fixed and positioned relative to the button required doing positioning-related calculations manually.
The more complex the layout, the more complex the situation. For example, if the table in the above example was in a scrollable container,
the position of the context menu would have to be updated manually on every scroll event.
With the CSS Anchor Positioning the solution to the above problem becomes trivial and requires 2 parts:
The <button> element must be marked as an anchor element by adding anchor-name: --some-name.
The context menu element must position itself using the anchor() function: left: anchor(--some-name right); top: anchor(--some-name bottom).
The above is enough for the web engine to understand that the context menu element’s left and top must be positioned to the anchor element’s right and bottom.
With that, the web engine can carry out the job under the hood, so the result is as in Figure 2:
As the above demonstrates, even with a few simple API pieces, it’s now possible to address very complex scenarios in a very elegant fashion from the web developer’s perspective.
Moreover, CSS Anchor Positioning offers even more than that. There are numerous articles with great examples such as
this MDN article,
this css-tricks article,
or this chrome blog post, but the long story short is that
both positioning and sizing elements relative to anchors are now very simple.
The first draft of the specification was published in early 2023,
which in the web engines field is not so long time ago.
Therefore - as one can imagine - not all the major web engines support it yet. The first (and so far the only) web engine
to support CSS Anchor Positioning was Chromium (see the introduction blog post) -
thus the information on caniuse.com.
However, despite the information visible on the WPT results page,
the other web engines are currently implementing it (see the meta bug for Gecko and
bug list
for WebKit). The lack of progress on the WPT results page is due to the feature not being enabled by default yet in those cases.
From the commits visible publicly, one can deduce that the work on CSS Anchor Positioning in WebKit has been started by Apple early 2024.
The implementation was initiated by adding a core part - support for anchor-name, position-anchor, and anchor(). Those 2 properties and function are enough to start using the feature
in real-world scenarios as well as more sophisticated WPT tests.
The work on the above had been finished by the end of Q3 2024, and then - in Q4 2024 - the work significantly intensified. A parsing/computing support has been added for numerous
properties and functions and moreover, a lot of new functionalities and bug fixes landed afterwards. One could expect some more things to land by the end of the year even if there’s
not much time left.
Overall, the implementation is in progress and is far from being done, but can already be tested in many real-world scenarios.
This can be done using custom WebKit builds (across various OSes) or using Safari Technology Preview on Mac.
The precondition for testing is, however, that the runtime preference called CSSAnchorPositioning is enabled.
Since the CSS Anchor Positioning in WebKit is still work in progress, and since the demand for the set of features this module brings is high, I’ve been privileged to contribute
a little to the implementation myself. My work so far has been focused around the parts of API that allow creating menu-like elements becoming visible on demand.
The first challenge with the above was to fix various problems related to toggling visibility status such as:
The obvious first step towards addressing the above was to isolate elegant scenarios to reproduce the above. In the process, I’ve created some test cases, and added them to WPT.
With tests in place, I’ve imported them into WebKit’s source tree and proceeded with actual bug fixing.
The result was the fix for the above crash, and the fix for the layout being broken.
With that in place, the visibility of menu-like elements can be changed without any problems now.
The second challenge was about the missing features allowing automatic alignment to the anchor. In a nutshell, to get the alignment like in the Figure 3:
there are 2 possibilities:
The position-area CSS property can be used: position-area: bottom center;.
At first, I wasn’t aware of the anchor-center and hence I’ve started initial work towards supporting position-area.
Once I became aware, however, I’ve switched my focus to implementing anchor-center and left the above for Apple to continue - not to block them.
Until now, both the initial and core parts of anchor-center implementation have landed.
It means, the basic support is in place.
Despite anchor-center layout tests passing, I’ve already discovered some problems such as:
and I anticipate more problems may appear once the testing intensifies.
To address the above, I’ll be focusing on adding extra WPT coverage along with fixing the problems one by one. The key is to
make sure that at the end of the day, all the unexpected problems are covered with WPT test cases. This way, other web engines
will also benefit.
With WebKit’s implementation of CSS Anchor Positioning in its current shape, the work can be very much parallel. Assuming that Apple will keep
working on that at the same pace as they did for the past few months, I wouldn’t be surprised if CSS Anchor Positioning would be pretty much
done by the end of 2025. If the implementation in Gecko doesn’t stall, I think one can also expect a lot of activity around testing in the
WPT. With that, the quality of implementation across the web engines should improve, and eventually (perhaps in 2026?) the CSS Anchor Positioning
should reach the state of full interoperability.