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:
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 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:
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
Stencil is a toolchain for building highly optimized and 100% standards based Web Components that run in every browser:
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
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 filesrc/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:
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? ›
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:
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:
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";
@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:
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 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">
<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, 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
:
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:
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...
Whoa, it looks so beautiful 😍
Conclusions
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!