Building the Netflix UI with Ionic Last update: 2021-03-09
Building a complex UI with Ionic is not always easy, but can be learned by going through practical examples like we do today with the Netflix UI with Ionic!
You like to see popular apps built with Ionic? Check out my latest eBook Built with Ionic for even more real world app examples!
Get the Code đ
Receive all the files of the tutorial right to your inbox and subscribe to my weekly no BS newsletter!
In fact we will focus only on the home page of the original app, but that page already requires many different components that spice up the while view.
We will work with directives to fade the header out, create a semi transparent modal with blurred background and also a bottom drawer with backdrop - on top of the overall UI with horizontal slides and different sections on the page!
Starting the Netflix App
To get started we generate a tabs interface and add two pages to add another tab and a modal page for later. We also create the base for our custom directive and component that we will need in the end, so go ahead and run:
ionic start devdacticNetflix tabs --type=angular --capacitor
cd ./devdacticNetflix
# Additional Pages
ionic g page tab4
ionic g page modal
# Directive
ionic g module directives/sharedDirectives --flat
ionic g directive directives/hideHeader
# Custom component
ionic g module components/sharedComponents --flat
ionic g component components/drawer
ionic g service services/drawer
Additionally I created some mock data that you can download from Github and place inside the assets folder of your new app!
Because we want to load the local JSON data with Angular, we need to add two properties to our tsconfig.json:
"compilerOptions": {
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true
...
}
The previous commands also generated entries in our global routing file that we donât need so clean them up right now by changing the app/app-routing.module.ts to:
import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{
path: '',
loadChildren: () => import('./tabs/tabs.module').then((m) => m.TabsPageModule)
}
];
@NgModule({
imports: [RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })],
exports: [RouterModule]
})
export class AppRoutingModule {}
To style the application we can also change the default dark color inside the src/theme/variables.scss now:
/** dark **/
--ion-color-dark: #000000;
--ion-color-dark-rgb: 0,0,0;
--ion-color-dark-contrast: #ffffff;
--ion-color-dark-contrast-rgb: 255,255,255;
--ion-color-dark-shade: #000000;
--ion-color-dark-tint: #1a1a1a;
On top of that we will set the overall background of our app to black so we donât need to change this for each and every component.
You can apply the changes inside the src/global.scss like this:
:root {
/* Set the background of the entire app */
--ion-background-color: #000;
/* Set the font family of the entire app */
--ion-text-color: #fff;
}
Now we have a dark background and light text and can work on the different areas of our app.
Changing the Tabs UI
The first area to change is the tab bar, which should include another tab (that we generated in the beginning) so go ahead and include it inside the src/app/tabs/tabs-routing.module.ts:
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { TabsPage } from './tabs.page';
const routes: Routes = [
{
path: 'tabs',
component: TabsPage,
children: [
{
path: 'tab1',
loadChildren: () => import('../tab1/tab1.module').then((m) => m.Tab1PageModule)
},
{
path: 'tab2',
loadChildren: () => import('../tab2/tab2.module').then((m) => m.Tab2PageModule)
},
{
path: 'tab3',
loadChildren: () => import('../tab3/tab3.module').then((m) => m.Tab3PageModule)
},
{
path: 'tab4',
loadChildren: () => import('../tab4/tab4.module').then((m) => m.Tab4PageModule)
},
{
path: '',
redirectTo: '/tabs/tab1',
pathMatch: 'full'
}
]
},
{
path: '',
redirectTo: '/tabs/tab1',
pathMatch: 'full'
}
];
@NgModule({
imports: [RouterModule.forChild(routes)]
})
export class TabsPageRoutingModule {}
Just like we did in the Spotify UI with Ionic tutorial we will implement a logic to change the icon of a tab based on the selected tab.
This requires to listen for the ionTabsDidChange
event and storing the currently active tab, which can then be used inside the src/app/tabs/tabs.page.html to dynamically change the icon:
<ion-tabs (ionTabsDidChange)="setSelectedTab()">
<ion-tab-bar slot="bottom" color="dark">
<ion-tab-button tab="tab1">
<ion-icon [name]="selected == 'tab1' ? 'home' : 'home-outline'"></ion-icon>
<ion-label>Home</ion-label>
</ion-tab-button>
<ion-tab-button tab="tab2">
<ion-icon [name]="selected == 'tab2' ? 'copy' : 'copy-outline'"></ion-icon>
<ion-label>Coming Soon</ion-label>
</ion-tab-button>
<ion-tab-button tab="tab3">
<ion-icon [name]="selected == 'tab3' ? 'search' : 'search-outline'"></ion-icon>
<ion-label>Search</ion-label>
</ion-tab-button>
<ion-tab-button tab="tab4">
<ion-icon [name]="selected == 'tab4' ? 'download' : 'download-outline'"></ion-icon>
<ion-label>Downloads</ion-label>
</ion-tab-button>
</ion-tab-bar>
</ion-tabs>
Now we just need to implement a function which gets the current active tab whenever the tab changes, so we can keep track of the current value locally inside the src/app/tabs/tabs.page.ts like this:
import { Component, ViewChild } from '@angular/core';
import { IonTabs } from '@ionic/angular';
@Component({
selector: 'app-tabs',
templateUrl: 'tabs.page.html',
styleUrls: ['tabs.page.scss']
})
export class TabsPage {
@ViewChild(IonTabs) tabs;
selected = '';
constructor() {}
setSelectedTab() {
this.selected = this.tabs.getSelected();
}
}
The first part is finished, next step will be more challenging!
Building the Netflix Home View
For now we want to implement the basic home view with hero/spotlight section at the top, and different sliding sections for series.
First step is a preparation for later in order to use the directive and modal page from here later, so we need to include it in the imports of the src/app/tab1/tab1.module.ts like this:
import { IonicModule } from '@ionic/angular';
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Tab1Page } from './tab1.page';
import { Tab1PageRoutingModule } from './tab1-routing.module';
import { SharedDirectivesModule } from '../directives/shared-directives.module';
import { ModalPageModule } from '../modal/modal.module';
@NgModule({
imports: [
IonicModule,
CommonModule,
FormsModule,
Tab1PageRoutingModule,
SharedDirectivesModule,
ModalPageModule
],
declarations: [Tab1Page]
})
export class Tab1PageModule {}
Now we need some dummy data and for reference Iâll also add the basic JSON that is used on this home page which looks like this (that you can find on Github):
{
"spotlight": {
"id": 2,
"name": "Firefly",
"rating": "#5 in Germany Today",
"desc": "One found fame and fortune. The other love and family. Lifelong best friends who are as different as can be â and devoted as it gets."
},
"sections": [
{
"title": "Continue Watching for Simon",
"type": "continue",
"series": [
{
"id": 1,
"progress": 42,
"title": "Bridergton",
"season": "S1:E3"
},
{
"id": 2,
"progress": 80,
"title": "Lupin",
"season": "S1:E5"
},
{
"id": 3,
"progress": 12,
"title": "Money Heist",
"season": "S3:E4"
}
]
},
{
"title": "Netflix Originals",
"type": "original",
"series": [
{
"id": 1
},
{
"id": 2
},
{
"id": 3
}
]
},
{
"title": "Trending Now",
"type": "series",
"series": [
{
"id": 4
},
{
"id": 5
},
{
"id": 6
},
{
"id": 7
},
{
"id": 8
}
]
}
]
}
So we can extract the information for the spotlight section and an array with information about the different sections, and we will do this easily inside the src/app/tab1/tab1.page.ts:
import { Component } from '@angular/core';
import { ModalController } from '@ionic/angular';
import homeData from '../../assets/mockdata/home.json';
import { DrawerService } from '../services/drawer.service';
@Component({
selector: 'app-tab1',
templateUrl: 'tab1.page.html',
styleUrls: ['tab1.page.scss']
})
export class Tab1Page {
sections = homeData.sections;
spotlight = homeData.spotlight;
opts = {
slidesPerView: 2.4,
spaceBetween: 10,
freeMode: true
};
constructor(private modalCtrl: ModalController, private drawerService: DrawerService) {}
openInfo(series) {}
async openCategories() {}
}
Donât worry about the empty functions right now, we will come back to them in a later step.
Everything until here was basic preparation, and now finally dive into the view!
Letâs cover everything that we got in this first view:
- The header area holds no title or specific buttons, but a logo and bar with 3 buttons from which we will trigger out modal later
- The content area starts with the spotlight section that uses the image information from the JSON as background image
- We an empty div element that will be used to display a gradient across the background image
- The rest of the spotlight is piecing together elements, which will be put into position correctly later with CSS
Below the spotlight area starts the area with our slides, based on the sections of our JSON data (see above).
Overall itâs a basic dynamic slide setup, but for the type âcontinueâ we also want to change the appearance so it holds another progress bar block with two additional buttons, that will later trigger our drawer component.
Itâs hard to exactly describe how everything came together, I highly recommend you check out the video version of this tutorial (link at the end of the tutorial) or at least the part covering this initial screen setup!
For now, continue by changing the src/app/tab1/tab1.page.html to:
<ion-header #header class="ion-no-border">
<ion-toolbar>
<img class="logo" src="/assets/mockdata/logo.webp" />
<ion-row class="ion-justify-content-center ion-text-center">
<ion-col size="4" class="ion-text-right"> TV Shows </ion-col>
<ion-col size="4"> Movies </ion-col>
<ion-col size="4" tappable (click)="openCategories()" class="ion-text-left">
Categories <ion-icon name="caret-down-outline"></ion-icon>
</ion-col>
</ion-row>
</ion-toolbar>
</ion-header>
<ion-content [fullscreen]="true">
<div
class="spotlight"
[style.background-image]="'url(/assets/mockdata/'+spotlight.id+'-cover.jpg)'"
>
<div class="gradient"></div>
<div class="info">
<img class="title" [src]="'/assets/mockdata/'+spotlight.id+'-title.webp'" />
<span class="rating">{{ spotlight.rating }}</span>
<ion-row class="ion-align-items-center">
<ion-col size="4" class="ion-tex-center">
<div class="btn-vertical">
<ion-icon name="add"></ion-icon>
<span>My List</span>
</div>
</ion-col>
<ion-col size="4" class="ion-tex-center">
<div class="btn-play">
<ion-icon name="play" color="dark"></ion-icon>
<span>Play</span>
</div>
</ion-col>
<ion-col size="4" class="ion-tex-center">
<div class="btn-vertical">
<ion-icon name="information-circle-outline"></ion-icon>
<span>Info</span>
</div>
</ion-col>
</ion-row>
</div>
</div>
<div *ngFor="let section of sections" class="ion-padding">
<span class="section-title">{{ section.title }}</span>
<ion-slides [options]="opts">
<ion-slide *ngFor="let series of section.series">
<img
*ngIf="section.type != 'continue'"
[src]="'/assets/mockdata/'+section.type+'-'+series.id+'.jpg'"
/>
<div class="continue" *ngIf="section.type == 'continue'">
<img [src]="'/assets/mockdata/'+section.type+'-'+series.id+'.jpg'" />
<div class="progress-bar">
<div class="progress" [style.width]="series.progress + '%'"></div>
</div>
<ion-row class="ion-no-padding">
<ion-col size="6" class="ion-text-left ion-no-padding">
<ion-button fill="clear" color="medium" size="small">
<ion-icon name="information-circle-outline" slot="icon-only"></ion-icon>
</ion-button>
</ion-col>
<ion-col size="6" class="ion-text-right ion-no-padding">
<ion-button fill="clear" (click)="openInfo(series)" color="medium" size="small">
<ion-icon name="ellipsis-vertical" slot="icon-only"></ion-icon>
</ion-button>
</ion-col>
</ion-row>
</div>
</ion-slide>
</ion-slides>
</div>
</ion-content>
Right now this screen looks quite ugly, but we will massively change the UI with our CSS rules.
Especially challenging was/is the gradient part:
Inside the Netflix app we can see a gradient behind the header area, and also at the end of the spotlight section. Therefore we use two different gradients that I generated with a CSS gradient tool.
On top of that we need to change the --offset-top
of our content to make sure the whole content starts directly at the top, otherwise the transparent toolbar background wouldnât really work.
Most other elements are minor changes to the appearance or position, so go ahead and change the src/app/tab1/tab1.page.scss to:
// General styling
ion-content {
--offset-top: 0px;
position: absolute;
}
.section-title {
font-weight: 600;
font-size: large;
}
ion-slides {
margin-top: 4px;
}
.continue {
background: #191919;
}
// Progress bar inside slides
.progress-bar {
height: 2px;
width: 100%;
background: #262626;
.progress {
background: #e40a15;
height: 100%;
}
}
// Spotlight section with gradient background
.spotlight {
width: 100%;
height: 50vh;
background-position: center;
background-size: cover;
margin-bottom: 20px;
position: relative;
// Image overlay gradient
.gradient {
background: linear-gradient(#ffffff00 40%, #000000c2, #000 95%);
width: 100%;
height: 100%;
}
.info {
width: 100%;
position: absolute;
bottom: 10px;
text-align: center;
img {
max-width: 50%;
}
.rating {
display: block;
font-weight: 700;
padding-top: 10px;
padding-bottom: 10px;
}
// Action button row
.btn-vertical {
font-weight: 500;
display: flex;
flex-direction: column;
align-items: center;
}
.btn-play {
background: #fff;
font-weight: 500;
border-radius: 2px;
color: #000;
padding: 4px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
}
}
// Header area
.logo {
margin-left: 16px;
width: 20px;
}
ion-toolbar {
--background: linear-gradient(
180deg,
rgba(0, 0, 0, 0.8715861344537815) 0%,
rgba(0, 0, 0, 0.8463760504201681) 57%,
rgba(0, 0, 0, 0.6923144257703081) 80%,
rgba(0, 0, 0, 0.5438550420168067) 89%,
rgba(0, 0, 0, 0) 100%
);
}
Worth looking at are also the btn-vertical
and btn-play
which show a way to create your own buttons using flexbox.
In many cases, the Ionic default buttons wonât work for your exact styling and by simply styling a custom div you get the benefits of the exact UI you want plus you can still combine this and use Ionicons in your button.
And with that, our basic home view is already finished now!
Adding a Fading Header Directive
Weâve already implemented a header like this in our Twitter UI with Ionic tutorial, and the idea is simple: React to scroll events of the content and transform the header however we want to. In our case, slowly scroll and fade it out!
To get started, include our generated directive inside the module we also generated at src/app/directives/shared-directives.module.ts and export it so we can use it later:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HideHeaderDirective } from './hide-header.directive';
@NgModule({
declarations: [HideHeaderDirective],
imports: [CommonModule],
exports: [HideHeaderDirective]
})
export class SharedDirectivesModule {}
The actual directive now keeps a reference to the header element and the children so we can fade out each of them separately (if we want to) and also implements the HostListener
to handle all scroll events of the content.
Whenever the content is scrolled, we calculate a new position and opacity for our header and apply it to that element. Feel free to play around with those values to fade/move the header area faster or slower in your own apps to get a feeling for how this directive works.
Now open the src/app/directives/hide-header.directive.ts and change it to:
import { Directive, Input, HostListener, Renderer2, AfterViewInit } from '@angular/core';
import { DomController, isPlatform } from '@ionic/angular';
@Directive({
selector: '[appHideHeader]'
})
export class HideHeaderDirective implements AfterViewInit {
@Input('appHideHeader') header: any;
private headerHeight = isPlatform('ios') ? 44 : 56;
private children: any;
constructor(private renderer: Renderer2, private domCtrl: DomController) {}
ngAfterViewInit(): void {
this.header = this.header.el;
this.children = this.header.children;
}
@HostListener('ionScroll', ['$event']) onContentScroll($event: any) {
const scrollTop: number = $event.detail.scrollTop;
if (scrollTop < 0) {
return;
}
let newPosition = -scrollTop / 2;
if (newPosition < -this.headerHeight) {
newPosition = -this.headerHeight;
}
let newOpacity = 1 - newPosition / -this.headerHeight;
this.domCtrl.write(() => {
this.renderer.setStyle(this.header, 'top', newPosition + 'px');
for (let c of this.children) {
this.renderer.setStyle(c, 'opacity', newOpacity);
}
});
}
}
Now we just need to apply this directive to our content area and pass the header element (to which we already added the #header
template reference before!) and activate the output of scroll events inside the src/app/tab1/tab1.page.html like this:
<ion-content [fullscreen]="true" scrollEvents="true" [appHideHeader]="header"></ion-content>
Whenever we scroll the content, the header now moves slowly up and fades our just like in the original Netflix app.
Creating an Ionic Modal with Blur & Animation
This part was maybe the most interesting for me personal as Iâve created transparent modals before, but not with a blurred background.
On top of that we want to apply a custom animation to the enter/leave of the modal since the Ionic default for iOS slides the modal in, which is not in line with the Netflix app.
You can find a more detailed explanation about building custom Ionic animations in my guest post on the Ionic blog as well.
Today, we will simply define a custom enter and leave animation which will only change the opacity of the modal DOM elements and not use any movement at all.
To do so, create a new file at src/app/modal-animation.ts and insert:
import { Animation, createAnimation } from '@ionic/angular';
//
// ENTER
//
export const modalEnterAnimation = (baseEl: HTMLElement, presentingEl?: HTMLElement): Animation => {
const backdropAnimation = createAnimation()
.addElement(baseEl.querySelector('ion-backdrop')!)
.fromTo('opacity', 0.01, 'var(--backdrop-opacity)')
.beforeStyles({
'pointer-events': 'none'
})
.afterClearStyles(['pointer-events']);
const wrapperAnimation = createAnimation()
.addElement(baseEl.querySelectorAll('.modal-wrapper, .modal-shadow')!)
.beforeStyles({ opacity: 0, transform: 'translateY(0)' })
.fromTo('opacity', 0, 1);
const baseAnimation = createAnimation()
.addElement(baseEl)
.easing('cubic-bezier(0.32,0.72,0,1)')
.duration(400)
.addAnimation([wrapperAnimation, backdropAnimation]);
return baseAnimation;
};
//
// LEAVE
//
export const modalLeaveAnimation = (
baseEl: HTMLElement,
presentingEl?: HTMLElement,
duration = 500
): Animation => {
const backdropAnimation = createAnimation()
.addElement(baseEl.querySelector('ion-backdrop')!)
.fromTo('opacity', 'var(--backdrop-opacity)', 0.0);
const wrapperAnimation = createAnimation()
.addElement(baseEl.querySelectorAll('.modal-wrapper, .modal-shadow')!)
.beforeStyles({ opacity: 0 })
.fromTo('opacity', 1, 0);
const baseAnimation = createAnimation()
.addElement(baseEl)
.easing('ease-out')
.duration(300)
.addAnimation([wrapperAnimation, backdropAnimation]);
return baseAnimation;
};
These animations could be applied globally to our Ionic app at the root module level, but if you only want to use them in some place you can also directly pass this to the create()
function of the modal like we do in here.
On top of that we will add the custom transparent-modal
class so we can apply our own styling in the next steps.
For now, open the src/app/tab1/tab1.page.ts and open the modal like this:
import { Component } from '@angular/core';
import { ModalController } from '@ionic/angular';
import homeData from '../../assets/mockdata/home.json';
import { ModalPage } from '../modal/modal.page';
import { modalEnterAnimation, modalLeaveAnimation } from '../modal-animation';
import { DrawerService } from '../services/drawer.service';
@Component({
selector: 'app-tab1',
templateUrl: 'tab1.page.html',
styleUrls: ['tab1.page.scss']
})
export class Tab1Page {
sections = homeData.sections;
spotlight = homeData.spotlight;
opts = {
slidesPerView: 2.4,
spaceBetween: 10,
freeMode: true
};
constructor(private modalCtrl: ModalController, private drawerService: DrawerService) {}
async openCategories() {
const modal = await this.modalCtrl.create({
component: ModalPage,
cssClass: 'transparent-modal',
enterAnimation: modalEnterAnimation,
leaveAnimation: modalLeaveAnimation
});
await modal.present();
}
openInfo(series) {}
}
Because the modal lives above the rest of our application, we now need to apply our CSS changes directly inside the src/global.scss:
.transparent-modal {
--background: #0000005c;
.modal-wrapper {
backdrop-filter: blur(12px);
}
ion-content {
--background: transparent;
}
}
This makes the modal background mostly transparent with a tint of black, but more importantly puts a blur on the background through which we will still see our epic home screen, but blurred! Now we just need to quickly load some data in our modal, which is actually just an array of strings. Go ahead by quickly changing the src/app/modal/modal.page.ts to:
import { Component, OnInit } from '@angular/core';
import { ModalController } from '@ionic/angular';
import categoryData from '../../assets/mockdata/categories.json';
@Component({
selector: 'app-modal',
templateUrl: './modal.page.html',
styleUrls: ['./modal.page.scss']
})
export class ModalPage implements OnInit {
categories = categoryData;
constructor(private modalCtrl: ModalController) {}
ngOnInit() {}
dismiss() {
this.modalCtrl.dismiss();
}
}
The modal itself looks really simple, since we only want to display a list of categories in the center of the view, which can be done with the Ionic grid quite fast.
On top of that we want to keep the close button fixed at the bottom, and in such cases the ion-footer
is a great place for stuff like that.
Open the src/app/modal/modal.page.html and change it to this:
<ion-content [fullscreen]="true">
<ion-row>
<ion-col size="12" *ngFor="let cat of categories" class="ion-text-center"> {{ cat }} </ion-col>
</ion-row>
</ion-content>
<ion-footer class="ion-text-center">
<ion-toolbar>
<ion-button (click)="dismiss()" fill="clear" color="light">
<ion-icon name="close-circle" slot="icon-only"></ion-icon>
</ion-button>
</ion-toolbar>
</ion-footer>
Again, the whole view doesnât look exactly like we want to as we donât have a header and all elements are too close to the top (and too small as well).
Also, the toolbar is not yet transparent so we need to overwrite a few Ionic CSS variables and make our icon stand out a bit more by changing its font size at the same time.
Go ahead with the src/app/modal/modal.page.scss to wrap up the modal:
ion-row {
margin-top: 10vh;
}
ion-col {
margin-bottom: 30px;
color: var(--ion-color-medium);
}
ion-toolbar {
--background: transparent;
--border-style: none;
ion-button {
ion-icon {
font-size: 50px;
}
}
}
Now we can call the modal, got a custom Ionic animation to fade it in and out, and also got the transparent blurred background that I will 100% reuse in future apps again!
Presenting a Drawer
Now we want to slide in a simple component from the bottom, and we will use a logic based on my Ionic bottom drawer tutorial. Only this time we will skip the scroll gesture handling and âsimplyâ make it scroll in and add a dark backdrop overlay.
It would be quite easy, but theres a problem:
- We want to present the component above the tabs, so it canât be added to a child page of the tab bar
- We want to trigger the component from a child page, which actually has no really connection to the component itself
- We need a connection between the drawer and the presenting component to understand when the backdrop needs to be displayed or hidden
But letâs take it step by step and start by setting up the shared module to declare and export our custom component inside the src/app/components/shared-components.module.ts:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DrawerComponent } from './drawer/drawer.component';
import { IonicModule } from '@ionic/angular';
@NgModule({
declarations: [DrawerComponent],
imports: [CommonModule, IonicModule],
exports: [DrawerComponent]
})
export class SharedComponentsModule {}
The component template itself is pretty easy as we only want to dynamically show the title and the rest is some static data for now. Go ahead and open the src/app/components/drawer/drawer.component.html and change it to:
<div class="drawer ion-padding" #drawer>
<ion-row class="ion-align-items-center">
<ion-col size="10">
<h2>{{ title }}</h2>
</ion-col>
<ion-col size="2" class="ion-text-right">
<ion-button fill="clear" (click)="closeDrawer()" color="medium" size="large">
<ion-icon name="close-circle"></ion-icon>
</ion-button>
</ion-col>
<ion-col size="2">
<ion-icon name="information-circle-outline" size="large"></ion-icon>
</ion-col>
<ion-col size="10"> Episodes And info </ion-col>
<ion-col size="2">
<ion-icon name="download-outline" size="large"></ion-icon>
</ion-col>
<ion-col size="10"> Download Episode </ion-col>
<ion-col size="2">
<ion-icon name="thumbs-up-outline" size="large"></ion-icon>
</ion-col>
<ion-col size="10"> Like </ion-col>
<ion-col size="2">
<ion-icon name="thumbs-down-outline" size="large"></ion-icon>
</ion-col>
<ion-col size="10"> Not For Me </ion-col>
</ion-row>
</div>
Inside the drawer we now need to implement functions that can be called from the outside to open or close the drawer component. In those cases, we will simply transform the Y value to slide it in, or set it back to move it out with a small transition.
The close function is also used inside the component when clicking the X icon, so we need a way to inform the parent component that the component was dismissed in order to hide the backdrop.
Therefore we add an Event emitter so our component can emit values back to the parent component when it implements the right functionality. We will do so whenever the open state changes, meaning the backdrop should be shown or hidden!
Go ahead and change the src/app/components/drawer/drawer.component.ts to:
import { Component, ElementRef, EventEmitter, Output, ViewChild } from '@angular/core';
@Component({
selector: 'app-drawer',
templateUrl: './drawer.component.html',
styleUrls: ['./drawer.component.scss']
})
export class DrawerComponent {
@ViewChild('drawer', { read: ElementRef }) drawer: ElementRef;
@Output('openStateChanged') openState: EventEmitter<boolean> = new EventEmitter();
title = '';
constructor() {}
openDrawer(title) {
this.title = title;
const drawer = this.drawer.nativeElement;
drawer.style.transition = '.2s ease-in';
drawer.style.transform = `translateY(-300px) `;
this.openState.emit(true);
}
closeDrawer() {
const drawer = this.drawer.nativeElement;
drawer.style.transition = '.2s ease-out';
drawer.style.transform = '';
this.openState.emit(false);
}
}
In order to give the component a fixed width and correct position we now need to apply a bit of custom styling inside the src/app/components/drawer/drawer.component.scss:
.drawer {
position: absolute;
box-shadow: rgba(0, 0, 0, 0.12) 0px 4px 16px;
width: 100%;
border-radius: 4px;
bottom: -300px;
height: 300px;
z-index: 11;
background: #2b2b2c;
color: #fff;
}
That means, initially the component is outside of the view and not visible and it will come up from the bottom when the according function is called.
Since the component is used in the TabsPage but we want to trigger it from the Tab1Page, we now implement a super simple service that can be used by the different pages to coordinate the presentation more easily.
Open the src/app/services/drawer.service.ts and add this simple code, which broadcasts a new value to the Subject when the drawer should be opend:
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class DrawerService {
drawerOpen = new BehaviorSubject(null);
constructor() {}
openDrawer(title) {
this.drawerOpen.next({ open: true, title });
}
}
Now we can change the according function in oursrc/app/tab1/tab1.page.ts to call this service whenever the drawer should be shown, and we can also pass in the title that should change based on which card we clicked in the view:
openInfo(series) {
this.drawerService.openDrawer(series.title);
}
Finally, we need to add the actual component but before we do it, we need to import the shared module inside the src/app/tabs/tabs.module.ts:
import { IonicModule } from '@ionic/angular';
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { TabsPageRoutingModule } from './tabs-routing.module';
import { TabsPage } from './tabs.page';
import { SharedComponentsModule } from '../components/shared-components.module';
@NgModule({
imports: [IonicModule, CommonModule, FormsModule, TabsPageRoutingModule, SharedComponentsModule],
declarations: [TabsPage]
})
export class TabsPageModule {}
With that in place, we add the app-drawer
component below our tabs implementation, and we also add a custom div which acts as the backdrop to the page.
The backdrop will fade in our out based on the state of the component (which we will handle from the class), and to our component we add the openStateChanged
which means we can listen for the events from the event emitter we implemented before!
Go ahead and change our tab bar at src/app/tabs/tabs.page.html to:
<ion-tabs (ionTabsDidChange)="setSelectedTab()">
<div
class="backdrop"
[ngClass]="backdropVisible ? 'fade-in' : 'fade'"
tappable
(click)="closeDrawer()"
></div>
<ion-tab-bar slot="bottom" color="dark">
<ion-tab-button tab="tab1">
<ion-icon [name]="selected == 'tab1' ? 'home' : 'home-outline'"></ion-icon>
<ion-label>Home</ion-label>
</ion-tab-button>
<ion-tab-button tab="tab2">
<ion-icon [name]="selected == 'tab2' ? 'copy' : 'copy-outline'"></ion-icon>
<ion-label>Coming Soon</ion-label>
</ion-tab-button>
<ion-tab-button tab="tab3">
<ion-icon [name]="selected == 'tab3' ? 'search' : 'search-outline'"></ion-icon>
<ion-label>Search</ion-label>
</ion-tab-button>
<ion-tab-button tab="tab4">
<ion-icon [name]="selected == 'tab4' ? 'download' : 'download-outline'"></ion-icon>
<ion-label>Downloads</ion-label>
</ion-tab-button>
</ion-tab-bar>
</ion-tabs>
<app-drawer (openStateChanged)="toggleBackdrop($event)"></app-drawer>
Inside the tabs page we now need three things:
- Listen to the Subject of the drawer service, which means we should open the component
- React to changes inside
toggleBackdrop
to show or hide the backdrop when the drawer was closed directly from within the component - Implement the close function when clicking on the backdrop right here
Since this page can now access the drawer as a ViewChild
, the whole operation finally works together and we can change our src/app/tabs/tabs.page.ts to:
import { ChangeDetectorRef, Component, ViewChild } from '@angular/core';
import { IonTabs } from '@ionic/angular';
import { DrawerComponent } from '../components/drawer/drawer.component';
import { DrawerService } from '../services/drawer.service';
@Component({
selector: 'app-tabs',
templateUrl: 'tabs.page.html',
styleUrls: ['tabs.page.scss']
})
export class TabsPage {
@ViewChild(IonTabs) tabs;
selected = '';
@ViewChild(DrawerComponent) drawer: DrawerComponent;
backdropVisible = false;
constructor(private drawerService: DrawerService, private changeDetectorRef: ChangeDetectorRef) {
this.drawerService.drawerOpen.subscribe((drawerData) => {
if (drawerData && drawerData.open) {
this.drawer.openDrawer(drawerData.title);
}
});
}
setSelectedTab() {
this.selected = this.tabs.getSelected();
}
closeDrawer() {
this.drawer.closeDrawer();
}
toggleBackdrop(isVisible) {
this.backdropVisible = isVisible;
this.changeDetectorRef.detectChanges();
}
}
The last piece is the styling for our backdrop which should cover the whole screen with a slightly transparent dark background, and the CSS animations with a short transition to fade in/out the whole backdrop.
Add the last missing piece to the src/app/tabs/tabs.page.scss now:
.backdrop {
width: 100%;
height: 100%;
background: #000000d2;
z-index: 10;
position: absolute;
}
.fade {
transition: 0.4s linear all;
opacity: 0;
z-index: -1;
}
.fade-in {
transition: 0.4s linear all;
opacity: 1;
}
Now we can control the drawer component from different places, while itâs still visible above the tab bar.
This could have been a lot easier but since we had a more complex case and different pages involved, we had to implement the whole logic including the service like this!
Enjoy your finished Netflix UI with Ionic and play around with the different elements now to get a better feeling for all the cool things we added.
Conclusion
The Netflix UI didnât look like a huge challenge at first, but if you look closer at popular apps you can see what makes them great: Small details.
A fade in the right place here, a custom animation there and overall a composition of complex elements and behaviours.
But like weâve seen before in the Built with Ionic series - we can achieve almost the same results with Ionic!