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 <div41 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 <div53 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: