Blog.

Implementing iOS 14 Weather Widget As A Web Component

Weather Widget on iOS 14
Picture of Dennis Morello
Dennis Morello

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:

<login-button></login-button>

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 behavior, 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, attaching it to a DOM node and taking care of the interactions with other elements. Among others, we will here use Stencil.

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](https://facebook.github.io/react/docs/introducing-jsx.html) and TypeScript:

![Stencil component](https://www.datocms-assets.com/34575/1600765939-stencil-component.png)

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:

```bash

npm run generate weather-widget

`

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

```bash

? Which additional files do you want to generate? ›

Instructions:

↑/↓: 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](https://www.datocms-assets.com/34575/1600765950-weather-widget-component.png)

For a detailed explanation of what is going on here, please refer to the excellent Stencil [documentation](https://stenciljs.com/docs/introduction) – 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](https://www.datocms-assets.com/34575/1600765930-ios-14-weather-widget.png)

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:

```jsx {1,9-13}

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

@Component({

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 (

<Host>

<slot></slot>

</Host>

);

}

}

`

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:

```jsx

render() {

return (

<Host>

<div>

<h3 class="location">{this.location}</h3>

<h4 class="temperature">{this.temperature}°</h4>

</div>

<div>

<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>

</p>

</div>

</Host>

);

}

`

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](https://ionicons.com) icon library.

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

```html {9,14-22,24-28,31}

<!DOCTYPE html>

<html dir="ltr" lang="en">

<head>

<meta charset="utf-8" />

<meta

name="viewport"

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 -->

<script

type="module"

src="https://unpkg.com/ionicons@5.1.2/dist/ionicons/ionicons.esm.js"

></script>

<script

nomodule=""

src="https://unpkg.com/ionicons@5.1.2/dist/ionicons/ionicons.js"

></script>

<!-- normalize.css -->

<link

rel="stylesheet"

href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css"

/>

</head>

<body>

<weather-widget></weather-widget>

</body>

</html>

`

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](https://necolas.github.io/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](https://www.datocms-assets.com/34575/1600765925-first-weather-widget-preview.png)

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:

```css

: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](https://developer.mozilla.org/en-US/docs/Web/CSS/:host) 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](https://www.datocms-assets.com/34575/1600765947-weather-component-styled.png)

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:

```css

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](https://9to5mac.com/2020/06/23/ios-14-wallpapers-download).

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:

```ts {5,17-22}

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:

```html

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

`


More Stories

Follow these simple rules to not get into trouble while traveling.

Picture of Dennis Morello
Dennis Morello

Essential things you need to know when planning a vacation abroad

Picture of Kurtis Lowe
Kurtis Lowe