Implementing iOS 14 Weather Widget As A Web Component

At WWDC20, Apple announced iOS 14, the next major release of one of the most popular mobile OSes. Among the new features, they finally added the ability to put widgets on the home screen:

Widgets on the Home Screen on iOS 14

In this article, we will port the beautiful weather widget of iOS 14 to the web, implementing it as an universal Web Component.

Source Code & Demo

You can find the source code for this project at this GitHub repository. A live demo is also available here!

Web Components

Web Components is a name that refers to a stack of different technologies that enable us to create reusable custom HTML elements:


In particular, a Web Component is made up of 3 things:

  • Custom Elements – A set of JavaScript APIs that allow you to define custom elements and their behaviour, which can then be used as desired in your user interface.
  • Shadow DOM – A set of JavaScript APIs for attaching an encapsulated "shadow" DOM tree to an element — which is rendered separately from the main document DOM — and controlling associated functionality. In this way, you can keep an element's features private, so they can be scripted and styled without the fear of collision with other parts of the document.
  • HTML Templates – The <template> and <slot> elements enable you to write markup templates that are not displayed in the rendered page. These can then be reused multiple times as the basis of a custom element's structure.

For a deep dive into each of these terms, you can refer to the exhaustive MDN resources. The interesting thing to note is that they are supported by all major web browsers:

Web Components support on major Web browsers

There are a bunch of tools for building Web Components without having to manually define a custom element, creating a shadow DOM root, attacching it to a DOM node and taking care of the interactions with other elements. Among others, we will here use Stencil.


Stencil is a toolchain for building highly optimized and 100% standards based Web Components that run in every browser:

Stencil Website

It is developed and maintained by the Ionic Framework team and combines the best concepts of the most popular frameworks into a simple build-time tool, such as:

  • Virtual DOM
  • Async rendering (inspired by React Fiber)
  • Reactive data-binding
  • TypeScript
  • JSX
  • Static Site Generation (SSG)

Getting Started

To start a new Stencil project, type the following command into a terminal (make sure to have a recent LTS version of NodeJS and npm v6 or higher):

npm init stencil

Stencil can be used to create standalone components, or entire apps. After running init you will be provided with a prompt so that you can choose the type of project to start. In our case, let's choose component:

? Pick a starter › - Use arrow-keys. Return to submit.

   ionic-pwa     Everything you need to build fast, production ready PWAs
   app           Minimal starter for building a Stencil app or website
❯  component     Collection of web components that can be used anywhere

Next, we need to provide a name for the project, for example "ios-14-weather-widget". After that, a folder containing our project is created:

Stencil project structure

Stencil Project Structure

Our project contains a bunch of files and folders, including a configuration file for TypeScript (tsconfig.json) and one for the Stencil compiler (stencil.config.ts).

The src/ folder contains the source files for our project. The most important ones are:

  • src/index.html – The main HTML file
  • src/components/ – Contains our custom components (like any React project)

Anatomy Of A Stencil Component

Stencil components are created by adding a new file with a .tsx extension inside the src/components/ directory – the .tsx extension is required since Stencil components are built using JSX and TypeScript:

Stencil component

As we can see, the my-component/ folder contains other files along the .tsx one. In particular, it contains a CSS file (my-component.css), an unit test file (my-component.spec.ts) and an end-to-end test file (my-component.e2e.ts).

We can delete the exising src/components/my-component/ directory because we will generate one for our widget later.

Creating A New Component

We have two ways to create a new component: we can either manually create a folder under the src/components/ directory with a .tsx file inside it, or we can use the generate NPM script specified in package.json.

We choose the second way, since it is quicker and standard. So, for the weather widget component, type this on the terminal and hit Enter:

npm run generate weather-widget

Next, the generator script asks us which additional files we want to generate alongside the .tsx one:

? Which additional files do you want to generate? ›
    ↑/↓: Highlight option
    ←/→/[space]: Toggle selection
    a: Toggle all
    enter/return: Complete answer

◉   Stylesheet (.css)
◯   Spec Test  (.spec.tsx)
◯   E2E Test (.e2e.ts)

For the purpose of this article, we can choose only Stylesheet (.css), so the script generates a stylesheet file for our component. When we hit Enter, the final result is this:

Weather widget component

For a detailed explanation of what is going on here, please refer to the excellent Stencil documentation – in particular, the Components section covers everything we need to know.

The Weather Widget Component

We want to reproduce the original iOS 14 weather widget:

Original iOS 14 weather widget

To do so, our component needs to show the following information:

  • The location name
  • The temperature
  • An icon representing the weather conditions
  • A textual description of the weather conditions
  • The minimum and the maximum temperature of the day

So, we add some attributes that the component will expose publicly – sort of props in the React world. Let's do it by importing the Prop decorator from @stencil/core and by adding some class members decorated with it:

import { Component, ComponentInterface, Host, Prop, h } from "@stencil/core";

  tag: "weather-widget",
  styleUrl: "weather-widget.css",
  shadow: true
export class WeatherWidget implements ComponentInterface {
  @Prop() location: string = "Milan";
  @Prop() condition: string = "Mostly Sunny";
  @Prop() tempHigh: number = 32;
  @Prop() tempLow: number = 24;
  @Prop() temperature: number = 28;

  render() {
    return (

Note that we have initialized the props with hardcoded values – it is fine for now. Next, let's edit the render() method in order to display the data:

render() {
  return (
        <h3 class="location">{this.location}</h3>
        <h4 class="temperature">{this.temperature}°</h4>
        <ion-icon class="weather icon" name="sunny"></ion-icon>
        <p class="weather condition">{this.condition}</p>
        <p class="weather temperatures">
          <span class="temp-high">H:{this.tempHigh}°</span>
          <span class="temp-low">L:{this.tempLow}°</span>

This is a simple markup that implements the structure of the widget. We have also given classes to elements so we can easily style them using CSS. The curious thing to note here is that on line 9 we are using the <ion-icon> element: as you guessed, this is not a standard W3C HTML element; instead, it is a Web Component coming from the Ionicons icon library.

Before we can have a preview of what we did, let's edit the src/index.html to include what we need:

<!DOCTYPE html>
<html dir="ltr" lang="en">
    <meta charset="utf-8" />
      content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0"
    <title>iOS 14 Weather Widget</title>

    <script type="module" src="/build/ios-14-weather-widget.esm.js"></script>
    <script nomodule src="/build/ios-14-weather-widget.js"></script>

    <!-- Ionicons -->

    <!-- normalize.css -->

Let's quickly review what's changed:

  • On line 9 we just changed the page title.
  • On lines 14-22 we added the script for loading the Ionicons package.
  • On lines 24-28 we added Normalize.css, a CSS reset utility that makes browsers render all elements more consistently and in line with modern standards.
  • On line 31 we have replaced the predefined Web Component with the newly created <weather-widget>.

We can now have a preview of what we did! Type npm start in a terminal and hit Enter, then a browser window will automatically appear showing the index.html:

First weather widget preview

Ok, this definetely does not look like the original iOS weather widget yet, but look at the page inspector inside the Developer Tools: the DOM does contain a <weather-widget> element that the browser is actually understanding and rendering! 🤩

Pro Tip: From the page inspector panel, try to add a location attribute to the <weather-widget> element with a value of your choice, then see how the text "Milan" is replaced by what you specified!

Making The Component Beautiful

Now that we wrote down the structure of our component, let's style it trying to get as close as we can to the iOS original widget.

Open the src/components/weather-widget/weather-widget.css file and replace its content with:

:host {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  width: 256px;
  height: 256px;
  border-radius: 32px;
  color: #ffffff;
  background: linear-gradient(#3f8ab3, #6ab3d6);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
    Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
  padding: 24px;
  box-sizing: border-box;
  user-select: none;
  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);

.location {
  margin: 0;
  font-weight: 500;
  font-size: 20px;

.temperature {
  margin: 0;
  font-weight: 200;
  font-size: 64px;

.weather {
  margin: 0;
  font-weight: 400;
  font-size: 18px;

.weather.icon {
  color: #f8d24b;
  font-size: 28px;
  margin-bottom: 4px;

.weather.condition {
  margin-bottom: 4px;

.weather.temperatures {
  font-variant-numeric: tabular-nums;

.temp-high {
  margin-right: 8px;

The :host pseudo-class selects the DOM node to which the shadow DOM of our custom Web Component is attached. We are simply saying that it is a flex container of size 256x256 pixels, whose children flows in column and have space between them.

All the rules listed here have been inferred from the original Apple weather widget.

Once we save the file, the dev server automatically reloads the page, and this is the result:

Weather widget with style

Wow, it looks pretty close to the original one!

Tidying Things Up

For the sake of completeness, let's add a background image to the page and center the widget.

To do so, we need to add a global style in the container page, so let's create a src/global/ folder inside which we create a style.css file and an assets/ folder.

Then, open the src/global/style.css file and paste the following:

body {
  display: grid;
  place-items: center;
  height: 100vh;
  grid-auto-rows: 100vh;
  background-image: url("/assets/background.jpg");
  background-repeat: no-repeat;
  background-size: cover;

Then, copy the background image to the src/global/assets/background.jpg file – I chose one of the original iOS 14 wallpapers.

Now that we did this, we need to tell the Stencil compiler to take those 2 files into account. To do so, open the stencil.config.ts and add the highlighted lines:

import { Config } from "@stencil/core";

export const config: Config = {
  namespace: "ios-14-weather-widget",
  globalStyle: "src/global/style.css",
  taskQueue: "async",
  outputTargets: [
      type: "dist",
      esmLoaderPath: "../loader"
      type: "docs-readme"
      type: "www",
      copy: [
          src: "global/assets/background.jpg",
          dest: "assets/background.jpg"
      serviceWorker: null // disable service workers

Line 5 tells the compiler where our global stylesheet file is located, while lines 17-22 are needed in order to copy the background image to the final bundle.

The last thing to do is to update the src/index.html in order to add the compiled global stylesheet using the <link> tag inside the <head> element:

<link rel="stylesheet" href="/build/ios-14-weather-widget.css" />

Finally, stop the running dev server and start it again, so the new configuration file is reloaded, and...

Final Result

Whoa, it looks so beautiful 😍


What we built in this article is just a simple Web Component that receives some attributes and renders them.

In a future post, we will see how to read the real user location and how to automatically fetch the weather data.

Thanks for reading!

If you have thoughts or questions on this post, please mention me on Twitter!

More Stories

React Awesome Reveal

Meet my new library for animating React components using Intersection Observer API and CSS Animations!

You Will Stop Using LocalStorage

Goodbye LocalStorage, welcome Key-Value Storage!