At IonicThemes our work is to create Ionic Starter apps and guides to help developers like you building mobile and progressive web apps faster.
We create components and features for multiple use cases so you don't have to.
In this post we want to share with you the recipe we use at IonicThemes to build our starter templates. Hopefully you would learn how to approach the task of building reusable UI kits or design systems and create your own components to improve the efficiency in future projects.
I would like to start this post by talking about the term
Compositions are formed by adding new objects to the previous ones, so that each of the latter is part of the former.
The process of software development is breaking down large problems into smaller ones.
Then, we can build components to solve those smaller problems, and compose them together to form a complete application.
Look around: composition is everywhere!
Components can be very diverse. We could define a component as something that can be useful on its own or that can also be combined with another element to form a bigger one.
Now think about this idea recursively. Because composition has no limits. You can compose objects as many times as you want.
But, composition is not only about software, it's around us everywhere.
Almost everything in this world is made up of different elements or components and can be broken down into smaller pieces.
Those who like cooking (like me) will understand what I mean. Cooking is all about combining ingredients, flavors, textures, temperatures, times, among others, in order to create a much more elaborate element.
For some people this element can either be the final product that they will eat or the starting point to create another element.
And if you think of the process of food composition from the initial seeds that grow on the soil and all the factors that took part in that process, well, this is almost an infinite loop. There are endless ways of combining and composing elements.
Getting back to building Ionic applications, we can divide elements in two categories:
Let me show you a clear example of how we can apply composition when creating a classic Sign Up form.
Creating a Sign-up Form with Ionic components
For instance, let's start by combining a few inputs, labels, and a button. We can agree that this is a decent sign-up form, right?
But, let's go a step further and think about what will happen when the user submits the form with an invalid value. The app should suggest that there are existing errors which must be corrected. As a result, the user experience will be improved.
Also, adding a show/hide password component is really helpful to improve the UX for users on mobile devices or small screens.
Nowadays, we also need to add social sign in to the sign-up form, because nobody wants to remember another password. Also, adding social sign in to our mobile apps has a lot of benefits.
Do you see the difference between the first and the last form example? Picture this process as a composition of different elements, we combined different elements (labels, inputs, buttons, copy) and interactions (validations, hints, OAuth integrations) to drastically improve the usability and user experience for the sign-up form.
By having a bunch of individual pieces already built, you can combine them to create better experiences.
Well, that was just for the UI of a classic sign up page, but take a second to think of all the pieces and elements that any given app includes such as authentication flows, user management, all the UI components such as:
- Ionic lists
- Ionic modals
- Ionic cards
- Ionic menus
- Ionic tabs
- Ionic slides
- Image gallery in Ionic
- and this is just to mention a few.
As for functionalities:
- Maps in Ionic
- Ionic forms
- Navigation in Ionic
- notifications
- payments
- Ionic image handling
- and trust me, the list goes on and on.
In case you don't get my point, what I want to show you is how an app is built by composing hundreds of building blocks and functionalities.
So if you, as a developer, pretend to build every piece of your application..., you are absolutely out of your mind!
Composition enables us to use and reuse components. And these components can either be created by you or by others.
I mean, if you are like us that builds multiple Ionic apps, you've probably gone through the sign-up page a couple of times, so, there's no point in coding it each time.
You can focus on building it once and then reuse it as often as necessary and just customize the styles for each application.
I loved this quote from Eric Elliot, "A software developer who doesn't understand composition is like a home builder who doesn't know about bolts or nails."
When building an app there are many repetitive tasks that you can avoid. Like the example from the sign-up page we saw before.
Thanks to the composition of multiple elements, we can save a lot of time. This frees us from having to be experts regarding all the aspects of developing an app.
You do not have to be an expert in every aspect of creating an app. We just need to be good assemblers to combine all the building blocks that will result in our unique application.
What is more, the beauty of open source is collaboration. Being able to share your knowledge and extend the creation of other developers is something we have to be very grateful for. Personally, I learn a lot from seeing other people's code.
From my point of view, collaboration and community are also a big part of composition, because you can use an element built by someone else and use it as it is, or you can use it as a suitable starter to build something else.
Okay, enough talking about all this theory, let's see how to build any UI with Ionic.
One of the features I love the most about Ionic Framework is the immense amount of components that are ready to use, customize and extend.
These elements are also beautifully crafted as Web Components enabling encapsulation and allowing them to be portable and easily reused.
In addition to this, each of these Ionic components consists of one or more custom elements.
For example, a card can be composed of images, labels and buttons. And this happens with many other Ionic UI components.
Regarding web components, I won't go into details here but, if you aren't familiar with them or if you are interested in learning more, we have an extensive article about Web Components in Ionic Apps.
For the purpose of this post, let's divide elements in two categories:
- UI Elements
- Functionalities
There are different strategies we can use to customize and enhance Ionic components. Some of them are pretty straight forward, others require adding more code (HTML and CSS).
In the following sections I will explain how to progressively customize an Ion Checkbox using four different strategies.
Why a checkbox? Because I want to show you how we can achieve beautiful UIs through composition even with a dull and plain checkbox.
In the following image you can see a basic and raw Ion-Checkbox
component.
I broke the customization down into four levels of effort and complexity and I built some code examples for each of these scenarios, so you can better understand what I'm trying to explain here. You can download all the source code of this example app by clicking the GET THE CODE button from the beginning of this page.
- Ionic CSS variables and Shadow Parts
- Adding HTML and CSS
- Customization based on component events
- Customization based on functionalities
Let's go over each of them.
The most basic approach to customizing Ionic Components is using CSS variables and CSS Shadow Parts. With creativity, there's plenty you can achieve with this strategy, but sometimes it's not enough.
Ionic CSS variables
CSS variables are entities defined by CSS authors that contain specific values to be reused throughout a document. These variables allow a value to be stored in one place and then referenced in multiple other places. They also make it possible to change CSS dynamically at runtime.
These are a few of the ion-checkbox CSS Custom Properties:
ion-checkbox { --checkmark-color: var(--ion-color-success-contrast); --checkmark-width: 4px; --background-checked: var(--ion-color-success); --border-color-checked: var(--ion-color-success); --border-color: var(--ion-color-success); --border-radius: 10px; --size: 28px; }
For the purpose of this guide I created an Ionic demo app where I added examples of some customizations we can achieve using these Ionic CSS custom properties.
In the following code you can see how by changing some Ionic CSS custom properties we can customize the basic ion-checkbox to be square and red.
<ion-item> <ion-label>Square & Red</ion-label> <ion-checkbox slot="start" class="square-and-red"> </ion-checkbox> </ion-item>
.square-and-red { --border-radius: 0px; --border-color: red; --background-checked: red; --border-color-checked: red; }
These CSS variables exist at the component level and each Ionic component has a list of its available CSS Custom Properties.
Due to encapsulation, the author of the web component, in this case the Ionic team, has to expose all the properties that can be modified. It's like if each component had an API. I like to think of it like that.
You can find the list of the available properties of each Ionic Component on the Ionic's documentation.
Ionic CSS Shadow Parts
CSS Shadow Parts add the ability to target a CSS property on an element inside the shadow tree.
A shadow tree is a tree structure of DOM elements inside a Shadow root. Nodes within the shadow tree are not affected by anything applied outside the shadow tree, and vice versa.
And why do we care about this? Because a key aspect of Web Components is encapsulation, this means being able to keep the markup structure, styles, and behaviors hidden and separate from other code on the page so that different parts do not clash, and the code can be kept nice and clean.
So, shadow parts allow us to style elements inside a shadow tree, from outside that shadow tree.
In the following image you can see the internal structure of the Ionic checkbox element and on its shadow tree we can find the parts container
and mark
.
Ionic exposes these Parts to enable customization while preserving the encapsulation.
Let's see an example of how we can use this inside an Ionic app.
In this example we use the part mark
to access the checkmark icon and change it for another one like the star, or the heart as you can see on the demo app.
ion-checkbox { &::part(mark) { // this is the SVG of the checkbox icon d: path("M18.75 12H5.25M12 5.25V18.75V5.25Z"); } }
Also, I went a step further and added an animation to the heart icon. I love this kind of micro interactions. I think they make a huge impact on the overall user experience.
So, as you can see, CSS allows us to change a limited set of properties in a very easy and straightforward way.
Using CSS properties and shadow parts is the easiest and quickest way to customize the Ionic components, but it's a bit limited on its own.
The truth is, we can't create a complex UI just by changing these properties.
So, in this section we will compose our previous checkbox with other HTML and CSS elements in order to create a bigger and richer element like the one you can see in below.
The following code includes the same HTML as the one we used in our previous example and by wrapping it with other HTML elements and a few custom styles we can drastically change its appearance while preserving its functionality.
<ion-col size="6"> <div class="checkbox-content" [style.--img-src]="'url(https://images.pexels.com/photos/220911/pexels-photo-220911.jpeg?auto=compress&cs=tinysrgb)'"> <ion-label class="checkbox-title">Food</ion-label> <ion-checkbox class="checkbox-item" value="food" checked></ion-checkbox> </div> </ion-col>
These are the CSS styles I used:
.checkbox-content { background: var(--img-src), rgba(0, 0, 0, .40); background-size: cover; background-blend-mode: overlay; border-radius: 10px; display: flex; height: 100%; width: 100%; justify-content: space-between; aspect-ratio: 1 / 1; .checkbox-title { color: var(--ion-color-light); font-weight: bold; align-self: flex-end; margin: 8px; font-size: 20px; } .checkbox-item { --background: transparent; --border-color: var(--ion-color-light); --size: 32px; margin: 8px; } }
Basically we have a div
wrapper that I called checkbox-content
that has a background image with a grayish overlay and then the checkbox-title
class is for the label and the checkbox-item
class is for the checkbox itself.
If we put a list of these elements inside a row, and we assign 6 columns to each one, we will get this new UI by just composing the checkbox with a few other elements.
It looks completely different from the item in level 1, doesn't it?
There are cases when we need to go a step further and access the component's API in order to add more advanced styles or enhance its functionality.
That's why in this section I will add an Angular wrapper element to extend the customization possibilities to build the UI you can see in the following demo.
What I want to do here is to style the whole component, based on the internal checkbox state. Furthermore, I mean, checked
or unchecked
.
Unfortunately this is not possible with plain CSS because parent selectors are not part of the CSS specification yet.
An easy way to solve this is to add an Angular wrapper element to access the checkbox API and listen to its state changes.
So when wrapping the code from the component built in the previous step, we add a custom Angular element that will access the component's API.
<ion-col size="6"> <app-custom-checkbox class="custom-checkbox"> <div class="checkbox-content" [style.--img-src]="'url(https://images.pexels.com/photos/220911/pexels-photo-220911.jpeg?auto=compress&cs=tinysrgb)'"> <ion-label class="checkbox-title">Food</ion-label> <ion-checkbox class="checkbox-item" checked value="food"></ion-checkbox> </div> </app-custom-checkbox> </ion-col>
Below is the code for our angular custom component. It's a very simple angular component where we just get a reference to the ion checkbox in order to be able to listen to its ionChange
event and style our element accordingly by adding a CSS class.
import { Component, ContentChild, HostBinding } from '@angular/core'; import { IonCheckbox } from '@ionic/angular'; @Component({ selector: 'app-custom-checkbox', template: '<ng-content></ng-content>', styles: [':host { display: block; }'], }) export class CustomCheckboxComponent { // get access to the IonCheckbox element @ContentChild(IonCheckbox) checkbox: IonCheckbox; @HostBinding('class.checkbox-checked') isChecked: boolean; constructor() {} ngAfterContentInit() { // set the checked state this.isChecked = this.checkbox.checked; // subscribe to changes this.checkbox.ionChange.subscribe(changes => { this.isChecked = changes.detail.checked; }); } }
Regarding the styles, most of them are the same as we used in the previous steps.
.custom-checkbox { .checkbox-content { background: var(--img-src), rgba(0, 0, 0, .40); background-size: cover; background-blend-mode: overlay; display: flex; height: 100%; width: 100%; justify-content: space-between; aspect-ratio: 1 / 1; .checkbox-title { font-weight: 800; text-transform: uppercase; font-size: 24px; color: var(--ion-color-light); text-align: center; word-break: break-word; overflow: visible; position: absolute; width: 70%; left: 15%; // vertically centered top: 50%; transform: translateY(-50%); } .checkbox-item { --border-radius: 0px; --border-width: 10vw; --border-color: transparent; --border-color-checked: transparent; --background: transparent; --background-checked: rgba(var(--ion-color-secondary-rgb), .4); --checkmark-width: 2px; height: 100%; width: 100%; } } &.checkbox-checked { .checkbox-title { display: none; } } }
With this approach, you can see how the composition of elements allows more possibilities for customization.
Let me quickly show you more components I built around the checkbox element using this same technique.
The idea of showing you these other examples is to convince you that by using composition you can create different components to build different UIs but using the same base component (in this example, the checkbox).
Finally, let's mix some composition ideas with functionalities. By functionalities, I mean things like forms and input validations, social logins, maps, push notifications, camera access and image handling, multi-language, database integration and so on.
The same composition principles can be applied to these features because once you have developed them you can reuse them in all your projects.
For example, at IonicThemes blog we have lots of guides and tutorials that explain how to add different functionalities to your Ionic app.
And the idea behind those guides is that you can get the code and the context of that feature and apply it to your own project.
You can find tutorials about things like Firebase integration, Facebook, Twitter and Google social logins, Map, Geolocation, etc.
Because the Forms and Validations tutorial is one of the most popular articles from our blog, I decided to add that feature to our Ionic demo app by putting our checkbox inside a form with a validation that requires the user to select at least two checkboxes before submitting it.
In the following code I created an Angular Form with the checkboxes inside. Here we are using Angular Reactive Forms but if you prefer to use another framework — because as you may know, Ionic is framework-agnostic — feel free to create the form using your framework of choice.
<form [formGroup]="exampleForm" (ngSubmit)="onSubmit()"> <ion-row class="interest-options"> <ion-col size="6" formArrayName="interests" *ngFor="let interest of exampleForm.controls.interests.controls; let i=index;"> <app-custom-checkbox class="custom-checkbox"> <div class="checkbox-content" [style.--img-src]="interestsList[i].image"> <ion-label class="checkbox-title">{{interestsList[i].name}}</ion-label> <ion-checkbox class="checkbox-item" [formControl]="interest"></ion-checkbox> </div> </app-custom-checkbox> </ion-col> </ion-row> </form>
Basically what I'm doing here is creating an Angular FormArray
with a FormControl
for each checkbox and with a custom validator that requires the user to select at least two checkboxes in order to be able to submit the form.
constructor() { this.exampleForm = new FormGroup({ // FormArray aggregates the values of each child FormControl into an array interests: new FormArray( // creates a FormControl for each 'Interest' from our interestsList // defined above and sets its selected value to 'true' or 'false' this.interestsList.map(x ==> new FormControl(x.selected)), // adds a custom validator that requires at least 2 checkboxes to be selected CheckboxCheckedValidator.minSelectedCheckboxes(2) ) }); }
Remember that you can get the entire code of this demo app with all the customization examples by clicking the GET THE CODE button from the beginning of the page.
Stencil, among other things, allows you to build reusable web components.
Stencil is a Web Component compiler for building fast, reusable UI components that run in every browser. Stencil is not a framework nor a library. It's a lightweight and straightforward compiler that turns classes with decorators into standards-based Web Components.
In fact, did you know that the Ionic team uses Stencil to build and deploy all their Ionic UI components as Angular, React, & Vue packages from one codebase? That's efficient!
These recent tweets from Max (the CEO of Ionic) summarize all the concepts behind this Ionic Customization Tutorial: Invest in reusable software instead of reinventing the wheel.
This is the recipe we use at IonicThemes to build all the user interfaces for our applications.
The secret here is that once you build a component for a specific use case, you can reuse it for many other use cases by just adapting a few styles and BOOM!, you have a new and totally different component.
This is what we do each time we create a new Ionic or Angular app, we reuse our existing components and functionalities and adjust them to build new UIs.
So with this post I want to encourage you to do the same. To show you that you don't have to build everything from scratch, you can reuse components to build something completely different.
You just need to bear this in mind for the next component you build. You need to try to create them as encapsulated as possible, so they may be easily reused in your other projects. This is where web components are going to be your best friends.
It can take an extra effort at the beginning but trust me, it will pay off in the future.
So, if there is one thing I want you to take from this tutorial is:
Create reusable components.
Invest in your code.
This post is based on my recent talk at the IonicConf 2021. Here is the video of my presentation.
Wrapping up this Ionic customization tutorial, keep in mind that these basic composition principles apply to all modern UIs.
Ionic UI components are a great starting point to build your applications but please, don't settle with defaults, dare to customize them and build rich interfaces.
Ionic provides you with all the base components and interactions, but then you'll have the responsibility to make them shine. In our blog we have lots of tutorials and example apps that you can use to kick-start your Ionic applications. Also, we have a section with dozens of free Ionic templates.
Don't forget to check our premium Ionic templates. I'm confident you would find them extremely useful to kick-start your next Ionic app.
As Max Lynch, the CEO of Ionic Framework said: "Any Ionic developer should consider getting one and getting to work that much more quickly!"
Hope you enjoyed this post. Thank you very much for reading and if you have any questions or feedback please leave a comment below.
Liked this Ionic Tutorial?
Leave your comments below.