June 28, 2022
  • Stencil
  • Tutorials
  • Capacitor
  • Design Systems
  • stencil
  • web components

Build Cross Platform Components with Stencil and Capacitor

Anthony Giuliano


The overwhelming majority of design systems that exist today are built for the web. While this is great for web developers, the web is not the only platform that people use. It doesn’t take a 10x developer to see that mobile apps are just as common as web applications. Yet, so many design systems seem to be built without the mobile experience in mind. In this tutorial, we’ll break out of this web centric mindset and see how we can build components that are suitable for the web and mobile. If you’re worried that we’ll have to use Swift or Java or Kotlin, fear not. We’re going to build cross platform components solely with web technologies using Stencil and Capacitor.

Stencil is a web components compiler that enables the components we write to be used with multiple different frontend frameworks. Capacitor is a cross-platform native runtime that enables the components we write to be used with multiple different platforms. Together, they can help us build cross-framework, cross-platform design systems that can be used in all kinds of projects across an entire organization.

In this tutorial, we’re going to be building an avatar component. This component, when clicked, will allow the user to change their avatar. On the web, the user will be able to select a new photo from the file system. On mobile, the user will be able to choose a photo from their photo gallery or take a new picture with the device camera. Here’s what it will look like.

Avatar Component on iOS Device

You can find all of the code for this tutorial in the stencil-capacitor-avatar-component github repo. If you prefer to watch a video tutorial, I recently created a video where I walk through this entire process. Check it out!

NOTE: This tutorial assumes that you already have a Stencil project created. To learn how to create a new Stencil project, check out the getting started docs.

Adding Capacitor to our Project

Before we get started creating our avatar component, we’ll first need to add Capacitor to our Stencil project. Start by installing Capacitor core and the Capacitor CLI.

npm install @capacitor/core
npm install @capacitor/cli --save-dev

After that, initialize Capacitor with

npx cap init

You’ll be prompted to provide an app name and ID here. Feel free to name your app whatever you would like.

The next steps are specific to the mobile platforms that you intend to support.


For our component to work with iOS, we will first need to install the @capacitor/ios package

npm install @capacitor/ios

And then add the iOS platform

npx cap add ios


Similarly, for our component to work with Android, we will need to install the @capacitor/android package

npm install @capacitor/android

And then add the Android platform

npx cap add android

Scaffolding the Component

Alright, with Capacitor installed and configured, we can now get started building our avatar component. Let’s start by creating a new component called my-avatar.

Our avatar component will have one prop called image, which will be the url to the avatar image. We should give it a default value in case an image is not provided (in the case of a new user). Because the value for this image will change when the user selects a new avatar, we’ll pass the option { mutable: true } to the @Prop decorator. For the component template, the only element we need is an <img /> tag to display the avatar image. Our component should look like this

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

  tag: 'my-avatar',
  styleUrl: 'my-avatar.css',
  shadow: true,
export class MyAvatar {
  @Prop({ mutable: true }) image: string = 'https://avatars.dicebear.com/api/micah/capacitor.svg';

  render() {
    return (
        <img src={this.image} alt="avatar" />

In the my-avatar.css file, we can add some styles to the img to size and shape it a bit.

img {
  height: 150px;
  width: 150px;
  border-radius: 99rem;
  background-color: #f1f1f1;
  object-fit: cover;

Using the Capacitor Camera Plugin

With the general structure of our component in place, it’s now time to start adding some functionality. To implement the ability to change the avatar image, we are going to leverage the Capacitor Camera Plugin. On mobile, this plugin provides the ability to take a photo with the device camera or to choose an existing one from the photo library. On the web, the plugin works just like an input element with type file. We can install the plugin with the following commands:

npm install @capacitor/camera
npx cap sync

Access to the device camera requires specific permissions to be configured. To learn how to configure those permissions for both iOS and Andriod, checkout the Capacitor Camera Plugin docs.

Getting a Photo

With the plugin installed and configured, we can now import the Camera and CameraResultType at the top of our avatar component.

import { Camera, CameraResultType } from '@capacitor/camera';

Now let’s write a function that will use the camera plugin to get a photo and set the component’s image.

handleClick = async () => {
  const photo = await Camera.getPhoto({
    quality: 90,
    allowEditing: true,
    resultType: CameraResultType.Uri,
  this.image = photo.webPath;

The Camera.getPhoto() function prompts the user to provide a photo from the photo library or to take a new photo with the camera. It accepts a series of options to further customize its behavior. You can learn more about those options in the API documentation.

At this point, we want to execute this function when our avatar image is clicked.

<img src={this.image} alt="avatar" onClick={this.handleClick} />

With this function defined and connected to the image, our avatar component should now work as expected! Let’s add the component to our index.html file to see it in action.


We can then run our project three different ways:

Web: npm run start
iOS: npx cap open ios and run with Xcode
Android: npx cap open android and run with Android Studio

Avatar Component on iOS Device

For an added bonus, I want to improve the component’s API by adding a custom event.

Improving the Component’s API

User’s of our avatar component will likely want to handle any change to the avatar photo. They may want to save it to a database for example. To improve the way consumers interact with our component, we can create a custom event that fires every time the avatar image changes. To do that, we first need to declare the event with the @Event() decorator.

@Event() myChange: EventEmitter<String>;

I’m calling the event myChange, but feel free to give the event a name specific to your design system. Next, we need to emit the event when the image is changed. We can do this in our handleClick function.

handleClick = async () => {
  const photo = await Camera.getPhoto({
    quality: 90,
    allowEditing: true,
    resultType: CameraResultType.Uri,
  this.image = photo.webPath;

Now users of our component can listen to when the image changes and perform the relevant actions when that event occurs.


  document.querySelector('my-avatar').addEventListener('myChange', () => {
    console.log('avatar image changed');

Cross Platform Design Systems

All in all, this avatar component is meant to showcase the flexibility of building components that are suitable for both the web and mobile. Building components with Capacitor and Stencil allows your design system to support multiple platforms and frameworks. With this strategy, you can even use your Stencil components in an existing native or React Native app with Portals. Design systems built in this way are so versatile that they can serve as the only design system that your company may need. With everyone pulling from the same design system, it is much easier to achieve consistency and feature parity across all of your applications.

As you play around with Capacitor in your Stencil projects, I encourage you to check out the documentation. There are tons of other great plugins to explore. I’m excited to see what kind of cross platform components you end up building!

Anthony Giuliano