Widgets!

A cute little red robot projecting a picture of itself on the wall. 3d render.
Credit: Aaron Gustafson × Designer

It was a long time coming, but I finally had a chance to put the work I did on a widgets proposal for PWAs into practice on my own site. I’m pretty excited about it!

Where it all started

I had the original idea for “projections” way back in 2019. Inspired by OS X’s Dashboard Widgets and Adobe AIR, I’d begun to wonder if it might be possible to project a component from a website into those kinds of surfaces. Rather than building a bespoke widget that connected to an API, I thought it made sense to leverage an installed PWA to manage those “projections.” I shared the idea at TPAC that year and got some interest from a broad range of folks, but didn’t have much time to work on the details until a few years later.

In the intervening time, I kept working through the concept in my head. I mean in an ideal world, the widget would just be a responsive web page, right? But if that were the case, what happens when every widget loads the entirety of React to render their stock ticker? That seemed like a performance nightmare.

In my gut, I felt like the right way to build things would be to have a standard library of widget templates and to enable devs to flow data into them via a Service Worker. Alex Russell suggested I model the APIs on how Notifications are handled (since they serve a similar function) and I was off to the races.

I drafted a substantial proposal for my vision of how PWA widgets should work. Key aspects included:

  • A declarative way to define and configure a widget from within the Web App Manifest;
  • A progressively enhanced pathway for devs to design a widget that adapts to its host environment, from using predefined templates to using custom templates to full-blown web-based widgets (with rendering akin to an iframe);
  • A collection of recommended stock templates that implementors should offer to support most widget types;
  • Extensibility to support custom templates using any of a variety of templating languages; and
  • A complete suite of tools for managing widgets and any associated business logic within a Service Worker.

Widgets became a reality

After continuing to gently push on this idea with colleagues across Microsoft (and beyond), I discovered that the Windows 11 team was looking to open up the new Widget Dashboard to third-party applications. I saw this as an opportunity to turn my idea into a reality. After working my way into the conversation, I made a solid case for why PWAs needed to be a part of that story and… it worked! (It no doubt helped that companies including Meta, Twitter, and Hulu were all invested in PWA as a means of delivering apps for Windows.)

While the timeline for implementation didn’t allow us to tackle the entirety of my proposal, we did carve out the pieces that made for a compelling MVP. This allowed us to show what’s possible, see how folks use it, and plan for future investment in the space.

Sadly, it meant tabling two features I really loved:

  • Stock/predefined templates. A library of lightly theme-able, consistent, cross-platform templates based on common data structures (e.g., RSS/Atom, iCal) would make it incredibly simple for devs to build a widget. If implemented well, devs might not even need to write a single line of business logic in their Service Worker as the browser could pick up all of the configuration details from the Manifest.
  • Configurable widget instances. Instead of singleton widgets, these would allow you to define a single widget type and replicate it for different use cases. For example, a widget to follow a social media user’s profile could be defined once and the individual instances could be configured with the specific account to be followed.

I’m sincerely hopeful these two features eventually make their way to us as I think they truly unlock the power of the widget platform. Perhaps, with enough uptake on the current implementation, we can revisit these in the not-too-distant future.


To test things out, I decided to build two widgets for this site:

  1. Latest posts
  2. Latest links

Both are largely the same in terms of their setup: They display a list of linked titles from this site.

Designing my widget templates

Given that they were going to be largely identical, I made a single “feed” template for use in both widgets. The templating tech I used is called Adaptive Cards, which is what Windows 11 uses for rendering.

Adaptive Card templates are relatively straightforward JSON:

{
"type": "AdaptiveCard",
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.6",
"body": [
{
"$data": "${take(items,5)}",
"type": "Container",
"items": [
{
"type": "TextBlock",
"text": "[${title}](${url})",
"wrap": true,
"weight": "Bolder",
"spacing": "Padding",
"height": "stretch"
}
],
"height": "stretch"
}
],
"backgroundImage": {
"url": "https://www.aaron-gustafson.com/i/background-logo.png",
"verticalAlignment": "Bottom",
"horizontalAlignment": "Center"
}
}

What this structure does is:

  1. Create a container into which I will place the content;
  2. Extract the first five items from the data being fed into the template (more on that in a moment);
  3. Loop through each item and
    • create a text block,
    • populate its content with Markdown to generate a linked title (using the title and url keys from the item object)
    • Set some basic styles to make the text bold, separate the titles a little and make them grow to fill the container; then, finally
  4. Set a background on the widget.

The way Adaptive Cards work is that they flow JSON data into a template and render that. The variable names in the template map directly to the incoming data structure, so are totally up to you to define. As these particular widgets are feed-driven and this site already supports JSONFeed, I set up the widgets to flow the appropriate feed into each and used the keys that were already there. For reference, here’s a sample JSONFeed item:

{
"id": "…",
"title": "…",
"summary": "…",
"content_html": "…",
"url": "…",
"tags": [ ],
"date_published": "…"
}

If you want to tinker with Adaptive Cards and make your own, you can do so with their Designer tool.

Defining the widgets in the Manifest

With a basic template created, the next step was to set up the two widgets in my Manifest. As they both function largely the same, I’ll just focus on the definition for one of them.

First off, defining widgets in the Manifest is done via the widgets member, which is an array (much like icons and shortcuts). Each widget is represented as an object in that array. Here is the definition for the “latest posts” widget:

{
"name": "Latest Posts",
"short_name": "Posts",
"tag": "feed-posts",
"description": "The latest posts from Aaron Gustafson’s blog",
"template": "feed",
"ms_ac_template": "/w/feed.ac.json",
"data": "/feeds/latest-posts.json",
"type": "application/json",
"auth": false,
"update": 21600,
"icons": [
{
"src": "/i/icons/webicon-rss.png",
"type": "image/png",
"sizes": "120x120"
}
],
"screenshots": [
{
"src": "/i/screenshots/widget-posts.png",
"sizes": "387x387",
"label": "The latest posts widget"
}
]
}

Breaking this down:

  1. name and short_name act much like these keys in the root of the Manifest as well as in shortcuts: The name value is used as the name for the widget unless there’s not enough room, in which case short_name is used.
  2. You can think of tag as analogous to class in HTML sense. It’s a way of labeling a widget so you can easily reference it later. Each widget instance will have a unique id created by the widget service, but that instance (or all instances, if the widget supports multiple instances) can be accessed via the tag. But more on that later.
  3. The description key is used for marketing the widget within a host OS or digital storefront. It should accurately (and briefly) describe what the widget does.
  4. The template key is not currently used in the Windows 11 implementation but refers to the expected standard library widget template provided by the system. As a template library is not currently available, the ms_ac_template value is used to provide a URL to get the custom Adaptive Card (hence “ac”) template. The “ms_” prefix is there because it’s expected that this would be a Microsoft-proprietary property. It follows the guidance for extending the Manifest.
  5. The data and type keys define the path to the data that should be fed into the template for rendering by the widget host and the MIME of the data format it’s in. The Windows 11 implementation currently only accepts JSON data, but the design of widgets is set up to allow for this to eventually extend to other standardized formats like RSS, iCal, vCard, and such.
  6. update is an optional configuration member allowing you to set how often you’d like the widget to update, in seconds. Developers currently need to add the logic for implementing this into their Service Worker, but this setup allows the configuration to remain independent of the JavaScript code, making it easier to maintain.
  7. Finally, icons and screenshots allow us to define how the widget shows up in the widget host and how it is promoted for install.

When someone installs my site as a PWA, the information about the available widgets gets ingested by the browser. The browser then determines, based on the provided values and its knowledge of the available widget service(s) on the device, which widgets should be offered. On Windows 11, this information is routed into the AppXManifest that governs how apps are represented in Windows. The Windows 11 widget service can then read in the details about the available widgets and offer them for users to install.

An animated capture of Windows 11’s widget promotion surface, showing 2 widgets available from this site’s PWA.

Adding widget support to my Service Worker

As I mentioned earlier, all of the plumbing for widgets is done within a Service Worker and is modeled on the Notifications API. I’m not going to exhaustively detail how it all works, but I’ll give you enough detail to get you started.

First off, widgets are exposed via the self.widgets interface. Most importantly, this interface lets you access and update any instances of a widget connected to your PWA.

Installing a widget

When a user chooses to install a widget, that emits a “widgetinstall” event in your Service Worker. You use that to kickoff the widget lifecycle by gathering the template and data needed to instantiate the widget:

self.addEventListener("widgetinstall", event => {
console.log( `Installing ${event.widget.tag}` );
event.waitUntil(
initializeWidget( event.widget )
);
});

The event argument comes in with details of the specific widget being instantiated (as event.widget). In the code above, you can see I’ve logged the widget’s tag value to the console. I pass the widget information over to my initializeWidget() function and it updates the widget with the latest data and, if necessary, sets up a Periodic Background Sync:

async function initializeWidget( widget ) {
await updateWidget( widget );
await registerPeriodicSync( widget );
return;
}

The code for my updateWidget() function is as follows:

async function updateWidget( widget ) {
const template = await (
await fetch(
widget.definition.msAcTemplate
)
).text();
const data = await (
await fetch(
widget.definition.data
)
).text();

try {
await self.widgets.updateByTag(
widget.definition.tag,
{ template, data }
);
}
catch (e) {
console.log(
`Couldn’t update the widget ${tag}`,
e
);
}
return;
}

This function does the following:

  1. Get the template for this widget
  2. Get the data to flow into the template
  3. Use the self.widgets.updateByTag() method to push the template and data to the widget service to update any widget instances connected to the widget’s tag.

As I mentioned, I also have code in place to take advantage of Periodic Background Sync if/when it’s available and the browser allows my site to do it:

async function registerPeriodicSync( widget )
{
let tag = widget.definition.tag;
if ( "update" in widget.definition ) {
registration.periodicSync.getTags()
.then( tags => {
// only one registration per tag
if ( ! tags.includes( tag ) ) {
periodicSync.register( tag, {
minInterval: widget.definition.update
});
}
});
}
return;
}

This function also receives the widget details and:

  1. Looks to see if the widget definition (from the Manifest) includes an update member. If it has one, it…
  2. Checks to see if there’s already a Periodic Background Sync that is registered for this tag. If none exists, it…
  3. Registers a new Periodic Background Sync using the tag value and a minimum interval equal to the update requested.

The update member, as you may recall, is the frequency (in seconds) you’d ideally like the widget to be updated. In reality, you’re at the mercy of the browser as to when (or even if) your sync will run, but that’s totally cool as there are other ways to update widgets as well.1

Uninstalling a widget

When a user uninstalls a widget, your Service Worker will receive a “widgetuninstall” event. Much like the “widgetinstall” event, the argument contains details about that widget which you can use to clean up after yourself:

self.addEventListener("widgetuninstall", event => {
console.log( `Uninstalling ${event.widget.tag}` );
event.waitUntil(
uninstallWidget( event.widget )
);
});

Your application may have different cleanup needs, but this is a great time to clean up any unneeded Periodic Sync registrations. Just be sure to check the length of the widget’s instances array (widget.instances) to make sure you’re dealing with the last instance of a given widget before you unregister the sync:

async function uninstallWidget( widget ) {
if ( widget.instances.length === 1
&& "update" in widget.definition ) {
await self.registration.periodicSync
.unregister( widget.definition.tag );
}
return;
}

Refreshing your widgets

Widget platforms may periodically freeze your widget(s) to save resources. For example, they may do this when widgets are not visible. To keep your widgets up to date, they will periodically issue a “widgetresume” event. If you’ve modeled your approach on the one I’ve outlined above, you can route this event right through to your updateWidget() function:

self.addEventListener( "widgetresume", event => {
console.log( `Resuming ${event.widget.tag}` );
event.waitUntil(
updateWidget( event.widget )
);
});

Actions

While I don’t want to get too into the weeds here, I do want to mention that widgets can have predefined user actions as well. These actions result in “widget click” events being sent back to the Service Worker so you can respond to them:

self.addEventListener("widgetclick", event => {
const widget = event.widget;
const action = event.action;
switch ( action ) {
// Custom Actions
case "refresh":
event.waitUntil(
updateWidget( widget )
);
break;
}
});

For a great example of how a widget can integrate actions, you should check out the demo PWAmp project. Their Service Worker widget code is worth a read.

Result!

With all of these pieces in place, I was excited to see my site showing up in the Widget Dashboard in Windows 11.

A screenshot of Windows 11 showing the Widget Dashboard overlaying the desktop with this site installed as a PWA to the right. The "latest posts" and "latest links" widgets are shown.

You can view the full source code on GitHub:


I’m quite hopeful this will be the first of many places PWA-driven widgets will appear. If you’s like to see them supported elsewhere, be sure to tell your browser and OS vendor(s) of choice. The more they hear from their user base that this feature is needed, the more likely we are to see it get implemented in more places.

Addendum: Gotchas

In wiring this all up, I ran into a few current bugs I wanted to flag so you can avoid them:

  • The icons member won’t accept SVG images. This should eventually be fixed, but it was keeping my widgets from appearing as installable.
  • The screenshots members can’t be incredibly large. I’m told you should provide square screenshots no larger than 500px ×500px.

  1. Have you checked out Server Events? ↩︎


Webmentions

  1. @Aaron wow this for Android (and dare I dream iOS) would be so awesome! ???? Thanks for working on it and sharing the details. ????

Shares

  1. Professor von Explaino
  2. Apple Annie :prami:
  3. Patrick Brosset
  4. Thomas Steiner :chrome:
  5. Alex
  6. Tyler Sticka
  7. Callie ????️‍⚧️
  8. Amadeus Maximilian