cft

Create Custom Bottom Sheets in Ionic using CSS Grid and Angular Animations

Create a custom bottom sheet using CSS Grid by stacking your views along the z-axes and animating them using Angular Animations.


user

William Juan

2 years ago | 5 min read

Ionic has a lot of prebuilt components such as ion-modal and ion-action-sheet to layer components on top of the main view. In this post, I will show you another way you could stack your views using CSS Grid and some Angular Animations to create a custom bottom sheet.

See it in action 👇

Bottom Sheet in Action
Bottom Sheet in Action


I showcased this technique in one of the episodes on my Restaurant Speed Code Series on Youtube. You can check out this specific episode here

For those that prefer going through the source code, check out the repo here

File and Folder Structure

To keep the post more focused, I will skip going over the setup process and jump straight to the areas of the code where this is implemented. These are the list of files from the project (Github repo) that we will be using through the tutorial. Feel free to clone the repo and follow along 🤓

Folder Structure
Folder Structure

High-level Concept

If you look closely at the example at the beginning of the tutorial, you'll see that 2 layers are added to the view when the bottom sheet is opened. I will refer to these as the shade layer (the semi-transparent layer that dims the background) and the bottom sheet layer (the actual bottom sheet that gets animated from the bottom).

Here is what the views look like if you look at it from a 3d perspective where the layers are stacked in increasing z-indexes as you move towards the top.

Bottom sheet layout breakdown
Bottom sheet layout breakdown

Setting up the main view

There are a few different ways to get this stacking behavior. For this particular use case, I used a CSS grid (via Tailwind CSS). CSS grids, by default, stack the elements on the z-axis if you place them in the same row/column which makes it convenient for creating layers like our shade and bottom sheet layers.

Open up app.component.html and wrap everything inside ion-app with a div with display: grid 👇

src/app/app.component.html
src/app/app.component.html


We will come back and update this after creating the bottom sheet component.

Bottom sheet Component

Let's create a simple bottom sheet component. Since we will be displaying information about food, we'll name it FoodDetailsBottomsheetComponent. Let's also add some hardcoded values that we can use to render in our template.

src/app/shared/components/layers/food-details-bottomsheet/food-detaile-bottomsheet.component.ts
src/app/shared/components/layers/food-details-bottomsheet/food-detaile-bottomsheet.component.ts


The template contains three parts - container, shade and a bottom sheet

- container - a div that spans the full width and height of its parents that will contain the shade and bottom sheet layers. This container will be another CSS grid to be able to stack the shade and bottom sheet layers on top of each other

- shade - a single div that spans the full width and height of the container to dim the background

- bottom sheet - another div that will contain the contents of the bottom sheet. This div has the self-end class (translates to align-self: flex-end; in CSS) which aligns it to the bottom of the parent container.

src/app/shared/components/layers/food-details-bottomsheet/food-details-bottomsheet.component.html
src/app/shared/components/layers/food-details-bottomsheet/food-details-bottomsheet.component.html

Add it to App Component

Now that we have the bottom sheet built out, let's head back to the app.component.html and add the component's selector to the template.

src/app/app.component.html
src/app/app.component.html


If you run the app, you will see the main view of the app covered by the shade and bottom sheet layers (and we don't have any way to dismiss it right now 🤦)

Opened bottom sheet
Opened bottom sheet


Open and Close via Layers Service

There are also a lot of different ways you can control the opening and closing of the bottom sheet. You could use a state management system like ngrx or ngxs, rxjs's Subject or BehaviorSubject, or even regular variables and function calls. For simplicity, I will be using rxjs's BehaviorSubject to control the opening and closing of the bottom sheet.

To do this, we will create a layers.service.ts which will have an openFoodDetailsBottomsheet and a closeFoodDetailsBottomsheet function that we can call from anywhere within the app to open or close the bottom sheet. These functions will then update the layersSource$ BehaviorSubject which can then be used by the bottom sheet to listen for new changes and react accordingly.

src/app/core/services/layers.service.ts
src/app/core/services/layers.service.ts

Add Open on Food Card Click

Open src/app/features/lunch/lunch.page.html and add a click event to the div container of app-food-card 👇

src/app/features/lunch/lunch.page.html
src/app/features/lunch/lunch.page.html


Open src/app/features/lunch/lunch.page.ts and inject the layers service we created in the previous section. We will then need to call layer service's openFoodDetailsBottomsheet in our click event listener 👇

src/app/features/lunch/lunch.page.ts
src/app/features/lunch/lunch.page.ts

Add Close on Shade Layer Click

Similarly, we will need to add click event listeners and wire them up to close the bottom sheet when we click on the shade layer.

src/app/shared/components/layers/food-details-bottomsheet/food-details-bottomsheet.component.html
src/app/shared/components/layers/food-details-bottomsheet/food-details-bottomsheet.component.html
src/app/shared/components/layers/food-details-bottomsheet/food-details-bottomsheet.component.ts
src/app/shared/components/layers/food-details-bottomsheet/food-details-bottomsheet.component.ts

Open and Close Event Listeners

Now that we have the open and close triggers, all that is left is to listen to these events and display and hide the bottom sheet accordingly. We will add an isOpen$ observable that maps the layers service's layers$ observable to listen to only changes in the foodDetailsBottomsheet property.

src/app/shared/components/layers/food-details-bottomsheet/food-details-bottomsheet.component.ts
src/app/shared/components/layers/food-details-bottomsheet/food-details-bottomsheet.component.ts


We can then bind the isOpen$ variable to our template using an async pipe and conditionally display our component's outermost container using an *ngIf

src/app/shared/components/layers/food-details-bottomsheet/food-details-bottomsheet.component.html
src/app/shared/components/layers/food-details-bottomsheet/food-details-bottomsheet.component.html


Bottom sheet using only *ngIf without animation
Bottom sheet using only *ngIf without animation


Add Animations

We will be adding two different sets of animations for displaying and hiding the bottom sheet. A fade in and fade out for the shade layer, and a slide up and slide down for the bottom sheet.

Since these animations get triggered as elements get added and removed from the DOM, we can utilize Angular Animation's :enter and :leave transitions.

For more information on Angular Animations and how it works, you can check out their official docs or a reference I created with live examples 🤓


Fade Animation

src/app/core/animations/fade.animation.ts
src/app/core/animations/fade.animation.ts

Slide Animation

src/app/core/animations/slide.animation.ts
src/app/core/animations/slide.animation.ts

Wire up our Bottom sheet Component

To use the animations we created in the previous sections, we will need to import the animation in our component and add it to the animations array in our Component decorator.

src/app/shared/components/layers/food-details-bottomsheet/food-details-bottomsheet.component.ts
src/app/shared/components/layers/food-details-bottomsheet/food-details-bottomsheet.component.ts


We can then add the animations using their trigger names prefixed with the @ symbol to our UI elements.

src/app/shared/components/layers/food-details-bottomsheet/food-details-bottomsheet.component.html
src/app/shared/components/layers/food-details-bottomsheet/food-details-bottomsheet.component.html


Bottom sheet enter animation
Bottom sheet enter animation

Awesome! Let's try opening and closing the bottom sheet. It's animating! But wait, it's only animating when it is being opened, not when it's getting closed. This is expected behavior since the *ngIf is applied to the parent of the elements with the animation directives. In other words, when the parent is added to the DOM, the children will be added as well, which triggers the enter animation. However, when the parent is removed from the DOM, it doesn't know about the children having an animation that needs to be executed before removing them from the DOM, hence causing those animations to get skipped.

We can fix this by adding an animation directive on the parent that queries the children and executes animateChild. This causes the parent to wait until the children are done executing their animation before removing them from the DOM

src/app/shared/components/layers/food-details-bottomsheet/food-details-bottomsheet.component.html
src/app/shared/components/layers/food-details-bottomsheet/food-details-bottomsheet.component.html


src/app/shared/components/layers/food-details-bottomsheet/food-details-bottomsheet.component.ts
src/app/shared/components/layers/food-details-bottomsheet/food-details-bottomsheet.component.ts


Final Demo of Bottom Sheet
Final Demo of Bottom Sheet


Conclusion

This brings us to the end of the tutorial. I hope you enjoyed that. If you are interested in more content like this or have any questions let me know in the comments or tweet me at @williamjuan27.

Upvote


user
Created by

William Juan

I am a Frontend Developer working primarily in the web and hybrid mobile spaces. The majority of my work has revolves around the Angular ecosystem, including working with other Angular-related frameworks such as NativeScript and Ionic.


people
Post

Upvote

Downvote

Comment

Bookmark

Share


Related Articles