Modern Web Weekly

Modern Web Weekly

Modern Web Weekly #47

The modern web, tested and explained in plain English

Danny Moerkerke
Feb 25, 2025
∙ Paid
Modern Web Weekly #47
Photo by Gritte / Unsplash

New "Open in app" UI in Chrome 135

Chrome 135, currently in beta, has a slightly different "Open in app" UI which appears on the right in the address bar when a web app is opened that is also installed as a PWA.

Before, it would only open the PWA that was installed through Chrome but now on macOS, it also offers to open the web app if it was added to the Dock through Safari.

Open an installed web app in Chrome 135

In this screenshot, What PWA Can Do Today is the PWA and PWA Today.app is the web app added to the Dock through Safari.


Speech recognition on any MediaStreamTrack

And here's another exciting feature that is now being prototyped in Chrome 135: speech recognition in web apps has always been available for the microphone input only but now you can run speech recognition on any MediaStreamTrack of type audio.

You can now test this in Chrome Canary 135 with experimental web features enabled.

A MediaStream can be obtained from an <audio> , <video>, or <canvas> element for example or from an uploaded file. This stream usually contains one or more MediaStreamTrack objects that can be of type audio or video and the audio track can now be used for speech recognition.

For example, you can get the MediaStreamTrack of an <audio> element like this:

const audioPlayer = document.querySelector('audio');

// set an audio source
audioPlayer.src = ...;

// wait for metadata to be loaded
audioPlayer.onloadedmetadata = async () => {
  // play the audio
  await audioPlayer.play();

  // get the stream and its tracks
  const stream = audioPlayer.captureStream();
  const tracks = stream.getTracks();

  if (tracks.length > 0) {
    const audioTrack = tracks[0];
  }
}

You can now run speech recognition on this audio track. First you need to create a new SpeechRecognition object. Despite what is on the MDN speech recognition page, the available constructor is webkitSpeechRecognition, not SpeechRecognition. This will probably be solidified into SpeechRecognition in the future but for now, we'll have to use the one prefixed with webkit.

Speech recognition is started by calling the start() method of the speech recognition object. This method has been refactored to take an optional MediaStreamTrackargument:

const audioPlayer = document.querySelector('audio');

// set an audio source
audioPlayer.src = ...;

// wait for metadata to be loaded
audioPlayer.onloadedmetadata = async () => {
  // play the audio
  await audioPlayer.play();

  // get the stream and its tracks
  const stream = audioPlayer.captureStream();
  const tracks = stream.getTracks();

  if (tracks.length > 0) {
    const audioTrack = tracks[0];

    // create the speech recognition object
    const recognition = new webkitSpeechRecognition();

    // start speech recognition 
    recognition.start(audioTrack);
  }
}

Subject to change
The API for this may still change so we may even get a separate method for recognizing speech from a source other than the user's microphone. The reason for this is that with the current setup, it's not possible to detect support for recognizing speech from any MediaStreamTrack.

There's still a discussion taking place on what the exact API should be but for now, you can detect support like this:

const isMediaStreamTrackSupported = () => {
  const iframe = document.createElement("iframe");
  iframe.src = URL.createObjectURL(new Blob([], { type: "text/html" }));
  document.body.appendChild(iframe);
  
  const recognition = new iframe.contentWindow.webkitSpeechRecognition();
  iframe.remove();
  
  try {
    recognition.start(0);
    return false;
  } 
  catch (error) {
    return error.name == "TypeError";
  }
}

const mediaStreamTrackSupported = isMediaStreamTrackSupported();

Check out the demo in Chrome Canary 135:


Book a call with me

Are you struggling with issues in your (progressive) web app or do you need help with implementing certain features?

Whenever you need help, just book a one-hour call with me and I will do my absolute best to answer all your questions and solve your problems.

It's like having me on speed dial whenever you need me

I charge $100 for a one-hour call.

If you're not happy with what I provided you get your money back, no questions asked.


TIL: calc(2px * 8px) doesn't work

This week I was debugging a CSS calculation and couldn't figure out why it didn't work until I finally realized that multiplying two px values doesn't work.

I expected a calculation like calc(2px * 8px) to return 16px but this is not the case. To make it return 16px you need to do either calc(2 * 8px) or calc(2px * 8). It makes sense when I think of it now since something like calc(2rem * 8px) doesn't work either, but I expected it to work initially.

On the other hand, calc(2px + 8px) does work but calc(2px + 8) doesn't. So when you subtract or add up values that have unit like px, rem etc. both sides need to have a unit value that doesn't have to be the same. For example, you can do calc(2rem + 12px)but when you multiply two values only one may have a unit.

Of course, you can add, subtract, or multiply two values without a unit but in that case, you don't need calc: you can do (2 +8), no need for calc(2 + 8).

Now you know.


Masonry layout in Safari Tech Preview and Firefox

In case you missed it, the native CSS masonry layout is now supported in Safari Tech Preview and Firefox.

A masonry layout is a grid layout where the items don't necessarily have the same height and where items in a row rise up to fill up the gaps that are left in the row above it.

Here's a screenshot of a regular grid layout:

Regular grid layout

And here's the same layout as a masonry layout:

Masonry grid layout

Notice that items 5 to 8 have risen up to fill up the gaps left by items 1, 2, and 3 because these are lower than item 4, which is the tallest one that defined the row height in the regular layout.

In both browsers support is behind a flag so in Safari Tech Preview you need to go to Feature Flags in the Develop menu and then check the box "CSS Masonry Layout". In Firefox you need to navigate to about:config, search for "masonry" and then toggle the option layout.css.grid-template-masonry-value.enabled to true. Refresh the page and you should see the masonry layout.

You can resize the viewport and see that the grid is responsive. I would have expected Item 5 to be positioned under Item 1 and Item 6 under Item 3 but it seems to be the other way around. Perhaps this is expected of the masonry algorithm.

Using the masonry layout
To use this layout in your own web app, simply define a grid layout and define the columns you want with grid-template-colums. Then set grid-template-rows to masonry and add items with different heights to the grid:

.container {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  grid-template-rows: masonry; /* the magic */
  gap: 10px;
  padding: 10px;
}

You now have a masonry layout! 🎉

Check this demo on codepen.


Going buildless with import maps

Does your web app need a build step? Does it really need a build step?

My project What PWA Can Do Today only needed a build step because I had to import dependencies using bare import specifiers. These specifiers don’t point to an imported module file but rather the name of the dependency you want to import.

Something like this:

import { LitElement } from 'lit';

where the import specifier lit doesn't point to an actual file but when you use a build tool like Webpack or Rollup this somehow works.

This is not magic though. It’s simply the Node.js module resolution algorithm. Since all these build tools are Node.js applications, they can use this algorithm to find the correct files at build time.

In the browser, we of course don’t have that luxury. We can’t afford to make multiple HTTP requests to try and find the correct file as this would result in a lot of network overhead. So build tools are basically our only option to rewrite all bare module specifiers to the actual files they point to but these have their limitations as well.

While static imports like import { LitElement } from 'lit' can be statically analyzed, this is not true for dynamic imports like import(specifier) since it's impossible to statically analyze the strings passed to the import()function.

Furthermore, tools like Webpack and Rollup need to inject code into the source code of your web app to make everything work which can result in significant overhead.

Keep reading with a 7-day free trial

Subscribe to Modern Web Weekly to keep reading this post and get 7 days of free access to the full post archives.

Already a paid subscriber? Sign in
© 2025 Danny Moerkerke
Privacy ∙ Terms ∙ Collection notice
Start your SubstackGet the app
Substack is the home for great culture