Modern Web Weekly #66
The modern web, tested and explained in plain English
It’s been a while, but after an extended break, Modern Web Weekly is now back! 🎉
Can I compress it? Yes, you can!
If your web app needs to compress and decompress data, you no longer need an external library. The Compression Streams API is now available in all browsers and enables your app to (de)compress data using GZIP and DEFLATE.
As you can already tell from the name, the API processes the data to be compressed or decompressed as a stream, which makes it very easy to use. The idea is you take a stream of data and pipe it through a CompressionStream or DecompressionStream:
// compression
const compressedReadableStream = inputReadableStream.pipeThrough(
new CompressionStream("gzip")
);
// decompression
const decompressedReadableStream = compressedReadableStream.pipeThrough(
new DecompressionStream("gzip")
);
That’s basically all there is to this API, short and sweet!
You could use this to let users select a file with <input type="file"> and then let them download the compressed version of it. You can easily convert the selected File object to a stream using its stream() method:
const fileInput = document.querySelector('input');
fileInput.addEventListener('change', async () => {
const [file] = fileInput.files;
const fileStream = file.stream();
const compressedFileStream = fileStream.pipeThrough(
new CompressionStream('gzip')
);
...
}); You can also use this when your web app generates large files that your users can download. When you compress these files, downloading will be faster using less bandwidth.
But how about multiple files?
What if you wanted to create a Zip archive with multiple files? You could put them all in a single stream and then pipe that stream through CompressionStream, but that would give you back a single file, so you wouldn’t be able to retrieve the individual files.
You could use a library like JSZip to do this for you, but I created a file compressor component that is much smaller (7KB vs. 374KB), leverages CompressionStream and also takes care of the parsing that is needed to retrieve the individual files in a Zip archive.
You can find the FileCompressor component on Github, and I also added a demo to What PWA Can Do Today, where you can try it for yourself.
The curious case of the status bar on iOS 26
In November last year, I posted on X about a new bug in iOS 26.1, where the theme-color meta tag seemed to be ignored, causing the blue status bar of my PWA What PWA Can Do Today to disappear on the home page, while it was still displayed on the demo pages.
These demo pages all have a bar with position: fixed at the top of the screen, and strangely enough, the status bar was still visible there.
After some searching, I learned iOS had quietly stopped supporting the theme-color meta tag and stumbled upon a very helpful post on Reddit, where users mastermog, and andesco discussed this behavior and had actually done some testing to confirm it.
They had discovered that iOS now took the color for the status bar from any element with position: static near the top of the page and otherwise would fall back to the background-color of the <body> element.
Here’s a screen recording of this behavior on iOS 26.1:
On the homepage, there is no element with position: fixed near the top of the page and the background-color of the <body> element is #ffffff (white). On this page, iOS falls back to the background-color of the <body> element, so the status bar is also white.
On the demo page, there is a blue bar with position: fixed near the top of the screen, and in that case, iOS matches the color of the status bar with the background-color of this bar (blue).
Specifically, the Reddit users mastermog and andesco found that the element that dictates the color of the status bar needs to:
have
position: fixedbe within 4 pixels from the top or 3 pixels from the bottom;
be at least 80% wide on iOS or 90% wide on macOS
be at least 3 pixels high.
At the time of writing, I haven’t been able to find any official Apple documentation that describes this behavior, nor any motivation for why this was implemented like this.
To me, this behavior seems counterintuitive and unpredictable, but hey, we’re dealing with Apple here, so anything can be expected.
And it gets worse…
On iOS 26.2.1, the behavior is changed again, and now, when there is no fixed element near the top of the screen, the status bar will still fall back to the background-color of the <body> element, but when the page is scrolled up, the content of the page will now be displayed under the status bar, and the background color will change to a gradient. When there is a fixed element near the top of the screen, the behavior stays the same.
Here’s a screen recording of the behavior on iOS 26.2.1:
The behavior is not bad per se, but it will be unexpected to a lot of PWAs, since before, the only way to get content to display under the status bar was to include the now-deprecated apple-mobile-web-app-status-bar-style meta tag with the value black-translucent.
Another quirk in this behavior is that, whenever an element with position: fixed is displayed at the top of the page, the status bar will change to the background-color of that element.
This happens, for example, whenever you display a <dialog> at the top of the screen. In the following screen recording, I changed the CSS of the installation bottom sheet (which is a <dialog>) of What PWA Can Do Today to shift all the way up to the top of the screen. You can see that the status bar suddenly turns to grey (the background-color of the <dialog>) when that <dialog> is displayed:
This is usually not what you want…
How to deal with all this
For now, we are stuck with this behavior and only time can tell what Apple will implement in the future, or if this behavior will stay the way it is in future iOS versions.
After extensive testing, I can confirm the following for iOS 26.2.1:
if you want a status bar with a certain color and you are okay with content being displayed under the status bar when scrolling up, just set a
background-coloron<body>, the status bar will have that same colorif you want a status bar with a certain color that stays in place while scrolling up, put an element with a
background-color that has the value you want for the status bar andposition: fixedorposition: stickywithin 4px from the top of the page (topcan be 4px maximum). Itsheightshould be at least 1px, and itswidthat least 80% on iOSif you don’t want the color of the status bar to change whenever a
<dialog>(or any other element with eitherposition: fixedorposition: sticky) is displayed, make sure it’s at least 1px from the top of the page
If anything changes in future iOS versions, you’ll read it here first.
PWA Audit: on your way to a great PWA
Do you already have a PWA, but are you running into issues with performance, security, or functionality? Or are you not sure how to make your PWA better?
I can help you by running an audit of your PWA
I will evaluate it on more than 35 criteria and provide you with clear and actionable instructions on how to improve it. No generic stuff that you can get anywhere, but an in-depth quality checkup to get you on your way to a great PWA.
Some of the criteria I will evaluate it on are:
Installability
Cross-device and cross-platform compatibility
Offline support
Usability
Effective use of modern web APIs
Performance
Security
Your investment in the improvement of your PWA through the audit is €499, excluding VAT (where applicable).
If you want to request an audit or first would like to know more, you can
fill out the form or book a call with me.
Two hardly mentioned benefits of the Invoker Commands API
Invoker commands now have baseline availability, which means they’re available in the latest versions of all major browsers. You can now replace event handlers to open and close dialogs and popovers with the declarative HTML attributes command and commandfor, and in addition to that, you can specify your own custom commands.
These custom commands do require an event listener for the command event, and while this may seem unfortunate, it introduces a huge benefit for event handlers. When you have a group of buttons that all invoke an action on the same element, you would normally have to add a click handler to each button. But with invoker commands, you add a single event handler to the target of the actions.
This is rarely mentioned explicitly, but it’s a huge shift. Instead of an event handler for each button that needs to invoke an action, you now need just a single handler on the target.
Of course, you could also wrap all buttons in a parent element and set the click handler on that parent. Then, any click events from the buttons inside it will bubble up and can be handled by that single event handler on the parent.
But invoker commands take this even a step further.
I created an <audio-recorder> web component that can record audio from the microphone, play it, save it, and show it as a waveform or display a real-time frequency analysis of the audio.
It has a couple of buttons to start capturing audio, record it, play it, save it, and toggle the view between waveform and frequency analysis. Originally, each button needed its own event handler to invoke an action:
this.playButton = this.shadowRoot.querySelector('#play');
this.pauseButton = this.shadowRoot.querySelector('#pause');
this.captureAudioButton = this.shadowRoot.querySelector('#capture-audio');
this.stopCaptureAudioButton = this.shadowRoot.querySelector('#stop-capture-audio');
this.freqButton = this.shadowRoot.querySelector('#frequencies-button');
this.waveformButton = this.shadowRoot.querySelector('#waveform-button');
...
this.audioContainer.addEventListener('click', this.handleWaveformClick.bind(this));
this.playButton.addEventListener('click', this.playPause.bind(this));
this.pauseButton.addEventListener('click', this.playPause.bind(this));
this.freqButton.addEventListener('click', this.showFrequencyAnalyzer.bind(this));
this.waveformButton.addEventListener('click', this.showWaveform.bind(this));
this.captureAudioButton.addEventListener('click', this.captureAudio.bind(this));
this.stopCaptureAudioButton.addEventListener('click', this.stopCaptureAudio.bind(this));By using command and commandfor I could replace all these event handlers on each button with a single handler on the component itself. Here are a few of the buttons:
<button
id="capture-audio"
commandfor="container"
command="--capture">
<i class="material-icons">mic</i>
</button>
<button
id="stop-capture-audio"
commandfor="container"
command="--stop-capture">
<i class="material-icons">mic_off</i>
</button>
<button
id="play"
commandfor="container"
command="--play">
<i class="material-icons">play_arrow</i>
</button>
<button
id="pause"
commandfor="container"
command="--pause">
<i class="material-icons">pause</i>
</button>and this is (part of) the command event handler that wires the commands to the methods of the component:
this.container.addEventListener('command', ({action}) => {
const actions = {
'--capture': this.captureAudio,
'--stop-capture': this.stopCaptureAudio,
'--play': this.playPause,
'--pause': this.playPause,
'--record': this.recordAudio
...
}
if(actions[action]) {
actions[action].call(this);
}
});this.container is the root <div id=”container”> of the component on which the commands are invoked, and this is also the value of the command attribute for the buttons in the previous example.
Now, where invoker commands really shine is that I can now also add a couple of external buttons that are outside of the component and that can be used to invoke the same actions as the internal buttons, using command and commandfor as well.
This means I can use external buttons to control the component without having to define any event handlers. I just added the same command and commandfor attributes and it works:
<audio-recorder id="recorder"></audio-recorder>
<button id="play" commandfor="recorder" command="--play">
Play
</button>
<button id="pause" commandfor="recorder" command="--pause">
Pause
</button>
// more buttonscommandfor now refers to the id of <audio-recorder> and command to the same actions the internal buttons invoke.
A nice, declarative, and simple way to add external controls to a component without having to define any event handlers 🎉
The only extra thing we need for external controls is an extra command event handler on the component itself. This is because the command event doesn’t bubble, so the handler on the internal <div> doesn’t work for external controls, and only and event handler on the component itself doesn’t work for the internal buttons:
// for external controls
this.addEventListener('command', handleCommand);
// for internal controls
this.container.addEventListener('command', handleCommand);Check out the source on Github.

