Smooth Bottom Navigator with Secondary Actions

15 December 2022

Updated: 03 September 2023

Not dissimilar from my previous post on the expanding bottom nav this expands on the idea of animating between states in the navigator in a more global way, this version makes use of a similar animation/transition pattern but does so by modifying the heights as well as the absolute positioning of different components

The implementation below takes the overall concept to the simplest possible state, however some refinements that can still be made include being responsive to the size of the additional content as well as doing a more accurate calculation on how large the sliding tab should be and how it’s placed

The values for the padding/positions are very hardcoded, but in practice you’d likely want to make these respond to data provided and size appropriately in regards to rest of the component

Here’s the svelte code below:

1
<script>
2
import {
3
InboxIcon,
4
HomeIcon,
5
DatabaseIcon,
6
MessageCircleIcon,
7
MicIcon,
8
MusicIcon,
9
} from "svelte-feather-icons";
10
11
let selected = "home";
12
13
const icons = [
14
{
15
id: "home",
16
icon: HomeIcon,
17
},
18
{
19
id: "database",
20
icon: DatabaseIcon,
21
},
22
{
23
id: "message",
24
icon: MessageCircleIcon,
25
},
26
{
27
id: "mic",
28
icon: MicIcon,
29
},
30
];
31
32
$: selectedIndex = icons.findIndex((item) => item.id === selected);
33
$: expanded = selected === "mic";
34
</script>
35
36
<h1>{selected}</h1>
37
38
<div class="wrapper">
39
<div class="background" class:expanded class:collapsed={!expanded}>
40
<div
41
class="slider"
42
style="--left: {(selectedIndex / icons.length) * 100}%;--right: {((selectedIndex+1) / icons.length) * 100}%;"
43
/>
44
<div class="content" class:expanded class:collapsed={!expanded}>
45
<MusicIcon />
46
<p>Recording: 00:01:23</p>
47
</div>
48
</div>
49
50
<div class="items">
51
{#each icons as icon}
52
<div
53
class="item"
54
on:click={() => (selected = icon.id)}
55
on:keypress={console.log}
56
>
57
<svelte:component this={icon.icon} />
58
</div>
59
{/each}
60
</div>
61
</div>
62
63
<style global>
64
/* uses fixed postion in order to lock it to lock the component to the bototom of the screen */
65
.wrapper {
66
position: fixed;
67
width: 100vw;
68
bottom: 0px;
69
}
70
71
/* ensure the background and items are all placed the same since they need to overlap */
72
.items,
73
.background {
74
height: 0px;
75
position: absolute;
76
left: 0px;
77
bottom: 20px;
78
right: 0px;
79
max-width: 80vw;
80
margin-left: auto;
81
margin-right: auto;
82
}
83
84
.background {
85
border: solid 1px black;
86
border-radius: 16px;
87
background-color: white;
88
height: 52px;
89
90
transition: height 300ms ease-in-out;
91
}
92
93
.background.collapsed {
94
transition-delay: 150ms;
95
}
96
97
.background.expanded {
98
height: 100px;
99
transition-delay: 0ms;
100
}
101
102
.content {
103
overflow: hidden;
104
display: flex;
105
flex-direction: row;
106
gap: 16px;
107
height: 24px;
108
opacity: 1;
109
padding: 20px 40px;
110
transition: all 300ms ease-in-out;
111
}
112
113
.content.collapsed {
114
height: 0;
115
opacity: 0;
116
padding: 0px 40px;
117
transition-delay: 0ms;
118
}
119
120
.content.expanded {
121
height: 24px;
122
opacity: 1;
123
transition-delay: 150ms;
124
}
125
126
.content p {
127
margin: 0;
128
}
129
130
.slider {
131
position: absolute;
132
left: calc(var(--left) + 6px);
133
right: va(--left);
134
bottom: 6px;
135
height: 40px;
136
width: calc(var(--right) - var(--left) - 12px);
137
border-radius: 12px;
138
background-color: #87b5eb70;
139
transition: left 300ms ease-in-out;
140
}
141
142
.items {
143
display: flex;
144
flex-direction: row;
145
justify-content: space-around;
146
height: 40px;
147
}
148
149
/* set the icon color */
150
.item {
151
color: black;
152
}
153
154
/* select a better default font */
155
* {
156
font-family: Arial, Helvetica, sans-serif;
157
margin: 0;
158
padding: 0;
159
}
160
</style>

And the current version of the component can be seen here: