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.
William Juan
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 👇

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 🤓
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.

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 👇
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.
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.
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.
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 🤦)

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.
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 👇
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 👇
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.
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.
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

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
Slide Animation
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.
We can then add the animations using their trigger names prefixed with the @ symbol to our UI elements.

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

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
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.

Related Articles