Widget Examples

Here is my Reddit code, it is not the cleanest, I copied / pasted / trial and error hacked until I got what I wanted. I have set the click to run script which then opens Reddit on that subreddit.

// Shows latest news from MacStories in a table.
// The table shows an image from the article, if available, and the title of the article.
let rReddit = "Scriptable"
let url = "https://www.reddit.com/r/" + rReddit + "/new.json?limit=6"
let req = new Request(url)
let json = await req.loadJSON()
let items = json.data.children
let appURL = "reddit:///r/" + rReddit

if (config.runsInWidget) {
  let widget = createWidget(items)
} else {

function createWidget(items) {
//   let item = items[0]
  let w = new ListWidget()
  w.backgroundColor = new Color("#47761E")

let header = w.addText("r/" + rReddit)
header.textColor = Color.black()


    for (item of items) {
      var myDate = new Date(item.data.created_utc * 1000)
      var myFormDate = addZero(myDate.getHours()) + ":" + addZero(myDate.getMinutes())
      let titleTxt = w.addText("• " + myFormDate + " - " + item.data.title)
        titleTxt.textColor = Color.white()
        titleTxt.textSize = 14
  var nowDate = new Date()
  var nowFormDate = addZero(nowDate.getHours()) + ":" + addZero(nowDate.getMinutes())
  let footer = w.addText("Last Updated: " + nowFormDate)
  footer.textColor = Color.black()
  return w

function addZero(i) {
  if (i < 10) {
    i = "0" + i;
  return i;

You’re so tempting me to try the iOS 14 beta… :slight_smile:


Thanks for posting that!

Will give that a try later today or this weekend (and probably start with the same widget size you use instead of a square one).

Using exactly your dimensions seems to work better!

Note: I intentionally made the background of the widget white to see what’s happening

@cranie Can you explain the dimensions? You use 1600x800 in Charty, but 300x1400 in Scriptable?

So excited for this!

1 Like

9 posts were split to a new topic: Radio Paradise Now Playing

I built a widget for informing about a storm nearby. It using some Polish SOAP service (http://burze.dzis.net), but should work for most of Europe (just replace coords). And… you need API key.

async function fetchData(position) {
  const DDtoDM = (coord) => {
    const coordInt = Math.floor(coord)
    return coordInt + (coord - coordInt) * 0.6;

  const KEY = 'KEY_KEY_KEY';
  const RADIUS = '12'; //km
  const stormURL = 'https://burze.dzis.net/soap.php';

  const requestXML = `
  <soapenv:Envelope xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:xsd='http://www.w3.org/2001/XMLSchema' xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:soap='https://burze.dzis.net/soap.php'>
     <soap:szukaj_burzy soapenv:encodingStyle='http://schemas.xmlsoap.org/soap/encoding/'>
        <y xsi:type='xsd:string'>${DDtoDM(position.longitude).toFixed(2)}</y>
        <x xsi:type='xsd:string'>${DDtoDM(position.latitude).toFixed(2)}</x>
        <promien xsi:type='xsd:int'>${RADIUS}</promien>
        <klucz xsi:type='xsd:string'>${KEY}</klucz>


  const req = new Request(stormURL);
  req.method = 'POST';
  req.headers = {
    'Content-Type': 'text/xml',
    'SOAPAction': '\'https://burze.dzis.net/soap.php\''
  req.body = requestXML;
  const response = await req.loadString();


  const parser = new XMLParser(response);

  var processedItem = '';

  var returned = {
    liczba: -1,
    odleglosc: -1,
    kierunek: 'n/a',
    okres: '0',

  parser.didStartElement = (name) => {
    processedItem = name;

  parser.foundCharacters = (value) => {
    switch (processedItem) {
      case 'liczba':
        returned.liczba = parseInt(value, 10);
      case 'odleglosc':
        returned.odleglosc = parseInt(value, 10);
      case 'kierunek':
        returned.kierunek = value;
      case 'okres':
        returned.okres = parseInt(value, 10);

  parser.didEndElement = (name) => {
    processedItem = '';



  return returned;

function createWidget(position, data) {
  let addressString = position.name;
  let headlineString = "No storms";
  let detailsString = "—";

  if (data.odleglosc > 0) {
    headlineString = "Storm nearby!";
    detailsString = `${data.liczba} strikes from ${data.kierunek} in last ${data.okres} min. Closest in ${data.odleglosc} km.`;

  const w = new ListWidget();
  w.backgroundColor = new Color("#20504F");

  const headline = w.addText(headlineString);
  headline.textColor = new Color("#fff");

  const details = w.addText(detailsString);
  details.textColor = new Color("#fff");

  const address = w.addText(addressString);
  address.textColor = new Color("#fff");


  return w;

const run = async (getPosition, getData, onFinish) => {
  const position = await getPosition();
  const data = await getData(position);

  onFinish(position, data);

const getStaticPosition = () => {
  return {
    longitude: 51.00,
    latitude: 18.00,
    name: 'Home',

await run(
  (position, data) => Script.setWidget(createWidget(position, data)),

1 Like

I have created super simple scriptable widget which queries api for Brainkeeper app (personal assistant for foreign vocabulary) to get random word from my list of words I collected. Super cool to have always foreign words in my sight to remember words better. :slight_smile:
Thanks for this Scriptable!


what if i wanted to make a widget like the coronavirus one but with a gradient background? Sorry, I’m new to coding

Several of the other widgets in this thread have a gradient background. Try taking a look at those to see how they do it. You’ll want to keep an eye out for mentions of LinearGradient and widget.backgroundGradient.

1 Like

My new widget shuffles a random album of the “Top 500 albums of all time” (according to the Rolling Stone magazine) and shows the cover art. Upon tapping on the widget it opens the album either in Spotify or plays it on your Sonos system.

Gist and setup instructions:


I wrote about this one:

Based on Matt Silverback’s original code, I added AQI calculations and set the widget to present the background color based on the current air quality status.

code here: https://gist.github.com/jasonsnell/4b458e2775e11ff7dd8b21dd26aa504e


My new widget shuffles one of your latest Pocket bookmarks and opens the article in Safari once you tap it.

I also built a shortcut which makes it easy to obtain your Pocket access token and stores the whole script at the end. Just save it to your scriptable folder in iCloud Drive.

Gist & HowTo: https://gist.github.com/marco79cgn/e05ca19ea2d15194bc7991f7efab8083


Update 24.09.2020: fixed fonts

My next one (Sorry I’m addicted):
I ported my Simpsons Randomizer shortcut to a widget. It shuffles a random episode of The Simpsons. Upon tapping on the widget it plays the episode on Disney+. ¡Ay, caramba!

Gist and instructions: https://gist.github.com/marco79cgn/ac9a8add1c7dc5a6749b751a1d2a05a4

Cool :+1:t2: Thanks for sharing!

PS: Reached you on Twitter, waiting for your response:-)

Okay. I must be dumb. What am I doing wrong? I can’t get a script to show up. I pulled the xkcd example (and others) and this is what I see when I select it from the widget…

Running beta 6

update: now supports build (155) - removed centerContent for addSpacer

I made a Nintendo Switch eShop widget, based on my Nintendo Switch eShop script.

The latest Scriptables update made it easier to debug widgets so I fixed a script I had been working on: a way to quickly see what is on the eShop. Tapping will take you to that game on the eShop’s website.

I wish I could make a shelf or a grid of these, but for now this is pretty cool.

Get it from the gist here


I made a widget that shows the most recent article on a Feedbin account. It uses etags for caching and won’t break even when there is no internet connection - it will just show the last item it downloaded and indicate that it’s cached.

Here’s the code:

let fm = FileManager.local()
const etagPath = fm.joinPath(fm.documentsDirectory(), `feedbin-widget-etag-${Device.name()}.txt`)
const itemPath = fm.joinPath(fm.documentsDirectory(), `feedbin-widget-item-${Device.name()}.json`)

const authHeader = btoa(Keychain.get("feedbin-auth"))
let headers = {"base": {"Authorization": `Basic ${authHeader}`}}
headers["unread_entries"] = Object.assign({}, headers["base"])
if (fm.fileExists(etagPath)) {
    headers["unread_entries"]["If-None-Match"] = fm.readString(etagPath)

let item = {}

try {
    item = await loadItem()
} catch {
    item = JSON.parse(fm.readString(itemPath))
    item.author += item.author != "" ? " — Cached" : "Cached"

let widget = createWidget(item)

if (config.runsInWidget) {

function createWidget(item) {
  const textColor = new Color("#ffffff")
  let gradient = new LinearGradient()
  gradient.colors = [new Color("#1c1c1e"), new Color("#0c0c0e")]
  gradient.locations = [0.5, 1]

  let w = new ListWidget()
  w.backgroundGradient = gradient
  w.setPadding(10, 20, 10, 20)
  let titleTxt = w.addText(item.title)
  titleTxt.textColor = textColor
  titleTxt.lineLimit = 2

  let authorTxt = w.addText(item.author)
  authorTxt.textColor = textColor
  authorTxt.textOpacity = 0.8
  authorTxt.textSize = 12

  let summaryTxt = w.addText(item.summary)
  summaryTxt.textColor = textColor
  summaryTxt.textOpacity = 0.8
  summaryTxt.lineLimit = 2

  w.url = item.url
  return w
async function loadItem() {
    const baseURL = "https://api.feedbin.com/v2/"
    const unreadURL = "unread_entries.json"
    const entryURL = "entries.json"
    // get latest unread entry id
    let unreadRequest = new Request(baseURL + unreadURL)
    unreadRequest.headers = headers["unread_entries"]
    const unreadResponse = await unreadRequest.load()
    if (unreadRequest.response.statusCode == "200") {
        const unreadEntryIDs = JSON.parse(unreadResponse.toRawString())
        const latestID = unreadEntryIDs[unreadEntryIDs.length - 1]
        const etag = unreadRequest.response.headers["Etag"]
        // get entry
        let entryRequest = new Request(baseURL + entryURL + `?ids=${latestID}`)
        entryRequest.headers = headers["base"]
        const unreadEntry = await entryRequest.loadJSON()
        const latestEntry = unreadEntry[0]
        if (latestEntry == undefined) {
            entry = {"title": "No unread articles", "author": "", "summary": "—"}
        } else {
            entry = latestEntry
        fm.writeString(itemPath, JSON.stringify(entry))
        fm.writeString(etagPath, etag)
        return entry

    } else if (unreadRequest.response.statusCode == "304") {
        // nothing changed, read from file
        return JSON.parse(fm.readString(itemPath))


Please note as of the latest beta build (155), any script that is calling calling topAlignContent(), centerAlignContent() or bottomAlignContent() on a ListWidget will now throw an error. I’m sorry, but I’ve replaced the functions during the beta with the more flexible addSpacer() function.

If you add a spacer at the top of your widget by calling addSpacer() before adding other elements, your content will be bottom aligned. If you add the spacer at the bottom of your widget, the content will be top aligned. You can also call addSpacer(5) to add a spacer with a fixed height of 5 pixels.

This may happen if the widget crashes for some reason. If you’re using scripts that for others, it’s probably not due to memory constraints. Can you try rebooting your device? Sometimes that helps, at least for a while. It would be interesting to know if that helped.