Quick Start

What's widget.json?

widget.json is a file format designed to push content from the web to your home screen.

With a small amount of JSON, you can create a widget-sized window to your favorite web content.

For a blog, you can make a widget with the latest post title plus a link to go directly to it. For a group of friends, you can make a private, shared scratch pad for notes or images. For developers, you can publish a widget.json from your GitHub Action or other CI. For DIYers, a widget.json could host a professional-looking weather widget powered by their IoT backyard weather station. For Patreon creators, a private widget.json just for your backers. You can make static widgets that simply shows an image you like and then when tapped, launches an iOS Shortcut to FaceTime your grandpa.

Once you've discovered or built a widget.json file that you like, you can use a widget.json client app to fetch and render it. Think of it like RSS for Widgets. Like RSS, widget.json files can be highly dynamic.

Widgets come in different sizes so you can make your home screen look the way you want.

Whatever web content is important to you, widget.json lets you package it up and put it at your fingertips.

For iOS users, Widget Construction Set is an example widget.json client app.

Getting Started

Let's look at a simple hello world widget. Here's what the finished result will look like:

Small purple widget with the text "hello world"

Here's the widget.json that powers it:

That's it!

If you're using Widget Construction Set, you can click here to add hello world directly to your iOS home screen! See Widget Construction Set for details on how one-click widget links work.

Screenshot of an iOS device with a purple widget on it

We'll be hosting our example widget.json files as GitHub Gists so you can easly fork them and have a hosted widget.json of your own. See if you can remix hello world to say hello to you or change the background color. Here's my remixed hello world widget that I hope my cat River Milk sees.

Special message for my cat River Milk

Here's the widget.json file for it.

Schema Speedrun

Let's take a quick look over what we just saw. To see the full schema overview, check the chapter widget.json Schema.

Starting with the metadata at the top:

{
  "name": "Hello World",
  "description": "My first widget.json.",
  "data": {
    "content_url": "https://wd.gt/"
  },
}

The name and description fields are used in client apps like Widget Construction Set. The data object is a convenient place to template in data that you use across multiple widget layouts. We'll talk more about data in the chapter Anatomy of a Widget. Our data contains only the field content_url. This field is required and will launch the specified URL.

For more details on content_url and other actions a widget can take, see Widget Actions.

Okay, onto the meat of the schema, layouts:

  "layouts": {
    "hello_small": {
      "size": "small",
      "styles": {
        "colors": {
          "cool_purple": {
            "color": "#626DFF"
          }
        }
      },
      "layers": [
        {
          "rows": [
            {
              "height": 12,
              "cells": [
                {
                  "width": 12,
                  "background_color_style": "cool_purple",
                  "text": {
                    "string": "hello world"
                  }
                }
              ]
            }
          ]
        }
      ]
    }
  }

Specifying layouts is the bulk of any widget.json file. Even in our simple example, the presentation instructions are the majority. We've got a whole chapter on Widget Layout, but here's a quick overview.

layouts is a dictionary of different layout objects. Here we only specify a single layout called hello_small. You can call the layout whatever you want, but you'll see that this name will be used in other parts of the schema later to hint clients how to show your layouts. You need to specify at least one valid layout, but you can have more. For example, you might present more data on a large size widget or maybe you offer another small widget with a different color scheme.

Once you have a layout name, you need to specify a layout size. See the chapter widget.json Schema for details.

Next, we can define some optional styles. These come in the form of colors and fonts. We're only using colors here. We define a new color called cool_purple. You can see further down we use cool_purple on the property background_color_style.

Lastly, we have a layers array. Each layer is drawn on top of the last. To make the grid easily divisible, layers are 12x12 units. You can see our first and only layer has one row with height 12 and that one row has one cell with width 12. Cells are the fundamental unit of content in widget.json. This first cell ends up taking up the entire layer and turning it cool_purple. This cell also places a text object with the content hello world into our widget.

What's Next

Now that we have the fundamentals of widget.json, take a look at Anatomy of a Widget to see what an actually useful widget might look like. To learn more about laying out a widget, take a look at Widget Layout.

Anatomy of a Widget

In the Quick Start chapter we learned the basics of building a simple, static widget. Now let's see what a more visually rich and data-driven widget might look like.

There are two things you need to know about me: 1) I run a micro weather station in my backyard and 2) I love the font Chalkduster. There are no existing widgets that combine my two passions. So let's make one. Here's what the final result should look like:

Small widget with a tasteful font choice

We're using a few more features of widget.json, so this will look a little bigger than the hello world widget.

A few things are new here. We pass along version to let clients know what to expect in the rest of the widget.json payload. The version is currently 1. We'll only change the version with major schema changes that would break existing clients.

You'll notice we're using the data property a lot more now. data is a great way to template in dynamic values without having to touch any layouts. In this case, we only have one layout, but once we start adding more, you'll want to avoid having to touch each one individually.

"data": {
  "content_url": "https://example.com",
  "weather_city": "Seattle",
  "weather_temp": "65°",
  "weather_icon": "https://.../sunny.png"
},

To use the data values, we can use data_ref on text and image objects. These replace the string and url values respectively.

"text": {
  "data_ref": "weather_city",
  "size": 12,
  "font_style": "cool_font",
  "color_style": "city_text_color",
  "justification": "left"
}

We're also taking advantage of more features in the styles object. Instead of defining simple colors, we define a nice linear gradient to represent the year-round perfect blue skies of Seattle.

"styles": {
  "colors": {
    "background_gradient": {
      "color": "linear-gradient(0deg, #76A5D5, #2469AA)"
    },
    "city_text_color": {
      "color": "#FFFFFFAA"
    }
  },
  "fonts": {
    "cool_font": {
      "family": "Chalkduster"
    }
  }
},

linear-gradient(0deg, #76A5D5, #2469AA) might look like a familiar declaration if you've seen CSS gradients. CSS gradients are powerful. widget.json gradients eschew some power for simpler declarations. The full details are in the chapter widget.json Schema, but in general, widget.json gradients only take a degree to rotate, a start color, and an end color.

Colors can be specified in the form of #RRGGBB or #RRGGBBAA.

Also in the styles block, we've defined a named font called cool_font. The fonts you can specify here are left up to the clients and will fallback to system fonts if not available. You probably won't need anything other than Chalkduster, but if you do, Widget Construction Set supports fonts available on iOS.

We've covered most of the local weather widget, but we've left a few topics for the chapter on Widget Layout.

Widget Layout

In Anatomy of a Widget we looked at a weather widget we built. Now let's take a deeper dive on how the layout works.

Layers

Widgets are layed out based on a grid system of 12 units by 12 units. All sizing values are relative to these grid units. Layers are drawn back to front, so the first layer specified is the furthest back and newer layers are put on top. Just like a painter laying down layers of paint on top of each other.

Let's take a look at our weather widget.json from before.

Small widget with a tasteful font choice

You can see in the styles section we define a background gradient and then our first layer is a full width and full height background using that gradient. Because we defined it first, it will be the furthest back layer and other layers will be drawn on top of it.

{
  "rows": [
    {
      "height": 12,
      "cells": [
        {
          "width": 12,
          "background_color_style": "background_gradient"
        }
      ]
    }
  ]
}

Let's look at the next layer but elide some of the content.

{
  "rows": [
    {
      "height": 1
    },
    {
      "height": 1.5,
      ...
    },
    {
      "height": 0.25
    },
    {
      "height": 3.5,
      ...
    },
    {
      "height": 1.5
    },
    {
      "height": 3,
      ...
    },
    {
      "height": 1.25
    }
  ]
}

Here's how those rows translate into the finished product:

Weather widget with magenta lines to show cell boundaries and labels to show heights

Each layer must have its rows add up to height 12 and all the cells in a row must add up to width 12.

Note that not all widgets are 1:1 squares, therefore the width unit is not always the same size as the height unit.

Padding

You might have noticed our use of padding in a few spots. There are two ways to move content around in a widget: empty cells and padding. Empty cells are great for coarse movement and laying things out relative to the entire widget's content. While padding helps with fine-grained movement of content. We used empty cells to give the city text some vertical breathing room by putting a row with "height": 1.5 above it. We can see the city name itself uses padding to shift the name off of the left edge a little bit:

{
  "height": 1.5,
  "cells": [
    {
      "padding": 1.25,
      "width": 12,
      "text": {
        "data_ref": "weather_city",
        ...
      }
    }
  ]
},

Padding is nice when you have the rough layout in place and you just want to nudge things a direction without changing other heights and widths in the layer. Padding is "paid for" by the current cell. The cell boundary stays in place and the content is padded within the cell.

You might have noticed that the padding in this example is only adding some breathing room to the left side but not the top and bottom. This is because by default text has a min_scale_factor set to 1.0. We'll cover min_scale_factor below.

Text

Text is one of the main content elements in a cell. You can see the full text object schema here, but at a minimum you need to specify one of either string for an inline string or data_ref for a referenced string from the data object. Other than the string itself, you may want to change it's size from the default of 18.

Text objects also support some dynamic sizing. In certain situations a string might be too long to render at the desired size, however shrinking the font size a little bit might make the full string renderable. This "auto-sizing" is disabled by default because min_scale_factor is set to 1.0 by default. However, if you wish to let the text shrink to accommodate longer, dynamic strings, you can change this to a value less than 1.0. min_scale_factor is a multiplier on the size of the string. In other words, if you specify min_scale_factor of 0.5 on a text object with size 18, the text is allowed to shrink to 9.

Image

Image is another cell content element. Images can be configured by either url which specify an inline image URL or by data_ref which looks up the URL from the data object using the string provided.

Images can be optionally masked with a circle mask by passing mask: circle on an image object.

Images are fetched and rendered with the image center point positioned at the cell center point. Images are scaled to fit their width to the cell width. Aspect ratio is preserved and if images exceed their top and bottom, those portions of the image are clipped.

Widget Actions

Launching URLs

Widgets by default have a single hit target driven by the data property content_url. When the widget is tapped, the URL specified will be launched. It's important to note that this URL doesn't need to just be an https:// website. Your device likely has many system scheme handlers and you can use those as well. For instance, you could make a widget with a picture of your favorite neighbor and specify content_url as sms://5558675309 or tel://5558675309. Maybe you have lots of favorite neighbors so this could be a randomized phone number!

In addition to system scheme handlers, apps often have their own. For example, you can launch the iOS Shortcuts app via URL scheme.

For widgets larger than small, cells can be hit targets that launch their own URL via link_url. These URLs can also be whatever scheme your device can handle. You could extend your favorite neighbor widget to include your top 3 neighbors and have the cell that contains their picture launch the approriate URL.

Here's an example of a widget.json providing multiple size layouts and providing a medium size widget with multiple hit targets: one to go to the YouTube channel and one to open a specific video.

Four widget examples of different layouts and sizes

widget.json Schema

All fields are required unless marked Optional.

Top Level Fields

name: String.

description: String.

version: Number. Use 1 for now. Optional. If unspecified, your widget might not render properly on clients.

default_layout: String. Must match an object name in layouts object. Used to tell client which layout to select when adding a new widget. Optional. Default to the first layout in layouts.

thumbnail_layout: String. Must match an object name in layouts object. Used to tell client which layout to show when space is limited. For instance in a list of widgets. This would typically be a simplified widget or logo. Optional. Default to the first layout in layouts.

layout_display_order: Array of Strings. Strings must match object names in layouts object. Used to tell clients the order to preview the widget layouts. Optional. Default to None. If unspecified, client can determine order.

content_last_modified: ISO-8601 Datetime String (e.g. "2022-08-20T14:15:08Z") Used by clients to prioritize new content. Optional. Default to None.

data: Dictionary of key value pairs used throughout the widget layouts. content_url should always be defined and will be launched when the user taps the widget in areas that link_url don't cover.

layouts: Dictionary of String to Layout object.

Layout Object Fields

size: String. Values are small, medium, large, extra_large. small may not use link_url in cells. extra_large do not render on iPhones. small is a square. medium is roughly two smalls placed side by side (wider than it is tall). large is roughly four smalls arranged in a square. extra_large is roughly four mediums arranged in a grid.

styles: Dictionary with two keys: color and fonts. color is a Dictionary of Color objects. fonts is a Dictionary of Font objects.

layers: Array of Layer objects.

Color Object Fields

color: String. Either a hexcode color (e.g. #FFFFFF55 or #00FF00) or a linear gradient (e.g. linear-gradient(0deg, #76A5D5, #2469AA)) Optional. Default None.

Font Object Fields

family: String. A font that is renderable on a client. See http://iosfonts.com for what Widget Construction Set can render. Optional. Default None.

Layer Object Fields

rows: Array of Row objects.

Row Object Fields

height: Number. Must be 12 or less. All heights in a layer must sum to 12.

cells: Array of Cell objects. Optional. Default None.

Cell Object Fields

width: Number. Must be 12 or less. All widths in a row must sum to 12.

background_color_style: String. Must match a key in styles.color dictionary. Optional. Default None. Undefined behavior if no background on the widget is set. Either an image or a background_color_style should be set as a full width, full height layer.

padding: Number. Must be less than 50% of width of the cell. Optional. Default None.

text: Text object. Optional. Default None.

image: Image object. Optional. Default None.

link_url: String. Url to visit when this cell is tapped. Only usable on medium, large, and extra_large layouts. Optional. Default None.

Text Object Fields

string: String. Not required if data_ref is specified.

data_ref: String. Must match a key in data. Not required if string is specified.

size: Float. Optional. Default 18.0.

font_style: String. Must match a key in styles.font dictionary. Optional. Default client system font.

color_style: String. Must match a key in styles.color dictionary. Optional. Default #FFFFFF.

weight: String. Values are light, medium, bold, extra_bold. Optional. Default medium.

justification: String. Values are left, center, right. Optional. Default center.

min_scale_factor: Number. Must be 1.0 or less. This specifies how small a font can be scaled before it stops being scaled and may experience clipping. For example, 0.5 here would mean that an 18pt font can go as small as 18 * 0.5 or 9pt. Optional. Default 1.0.

Image Object Fields

url: String. Not required if data_ref is specified.

data_ref: String. Must match a key in data. Not required if url is specified.

mask: String. Values are circle. Optional. Default None.

Widget Construction Set

Widget Construction Set is available for iOS devices on the App Store.

Launch URLs

Widget Construction Set supports a URL scheme of widget://.

You can programmatically add a widget by launching widget://add_widget?url=....

If you want to go meta, you could make a large widget that showed widget previews inside it. Tapping the image of a wiget preview could use link_url with the url of widget://add_widget?url=....

If you want to link to private widgets with the credentials embedded in the add link, you can pass extra query parameters of user and password. For example widget://add_widget?url=...&user=...&password=....

Widget Fetch Interval

Widget Construction Set fetches widgets around every 15 minutes. It isn't meant to be real-time, but should stay relatively up-to-date.

Privacy Policy

Widget Construction Set does not collect personally identifiable data. Widget Construction Set does not collect telemetry or other usage data. All data is stored on your device.

Open Source App

The Widget Construction Set app is open source and available on GitHub.

Private Widgets

You might find it useful to have widgets that are not publicly accessible. Widget Construction Set supports private widgets via HTTP Basic Authentication.

Private widgets should be hosted to return an HTTP 401 unless a valid auth header is provided. A valid auth header is one with the key Authorization and the value of Basic d293OnN1Y2hfaGFja2VyIQ== where d293OnN1Y2hfaGFja2VyIQ== is equal to the Base64 encoded value of username:password. For more info on HTTP Basic Authentication, check out this MDN document.

Publishing widget.json Files

Hosting

Static widget.json files can be stored as GitHub Gists.

For more dynamic widgets, you might consider a cloud option like AWS Lambda functions, Vercel, or building your own server on fly.io.

Publishing

We currently don't offer any CMS plugins. It would be cool if popular blog engines published to a widget.json file in addition to RSS files. If you build a plugin, let us know and we'll add it here!

Community

Looking for widgets? Want to show off your creation or get help?

Join the widget.json Discord

About

wd.gt, widget.json, and Widget Construction Set were created by Michael Alyn Miller and BJ Malicoat.

We had access to an internet that was playful, weird, unexpected, and inspiring. We love the vibrancy and texture that comes from people having access to simple tools to express themselves and create the things that they want. Widgets are an approachable and pervasive canvas to have fun with. We hope people will create things we can't imagine.