Smooth Bottom Navigator with Secondary Actions

Created: 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:

<script>
  import {
    InboxIcon,
    HomeIcon,
    DatabaseIcon,
    MessageCircleIcon,
    MicIcon,
    MusicIcon,
  } from "svelte-feather-icons";

  let selected = "home";

  const icons = [
    {
      id: "home",
      icon: HomeIcon,
    },
    {
      id: "database",
      icon: DatabaseIcon,
    },
    {
      id: "message",
      icon: MessageCircleIcon,
    },
    {
      id: "mic",
      icon: MicIcon,
    },
  ];

  $: selectedIndex = icons.findIndex((item) => item.id === selected);
  $: expanded = selected === "mic";
</script>

<h1>{selected}</h1>

<div class="wrapper">
  <div class="background" class:expanded class:collapsed={!expanded}>
    <div
      class="slider"
      style="--left: {(selectedIndex / icons.length) * 100}%;--right: {((selectedIndex+1) / icons.length) * 100}%;"
    />
    <div class="content" class:expanded class:collapsed={!expanded}>
      <MusicIcon />
      <p>Recording: 00:01:23</p>
    </div>
  </div>

  <div class="items">
    {#each icons as icon}
      <div
        class="item"
        on:click={() => (selected = icon.id)}
        on:keypress={console.log}
      >
        <svelte:component this={icon.icon} />
      </div>
    {/each}
  </div>
</div>

<style global>
  /* uses fixed postion in order to lock it to lock the component to the bototom of the screen */
  .wrapper {
    position: fixed;
    width: 100vw;
    bottom: 0px;
  }

  /* ensure the background and items are all placed the same since they need to overlap	 */
  .items,
  .background {
    height: 0px;
    position: absolute;
    left: 0px;
    bottom: 20px;
    right: 0px;
    max-width: 80vw;
    margin-left: auto;
    margin-right: auto;
  }

  .background {
    border: solid 1px black;
    border-radius: 16px;
    background-color: white;
    height: 52px;

    transition: height 300ms ease-in-out;
  }

  .background.collapsed {
    transition-delay: 150ms;
  }

  .background.expanded {
    height: 100px;
    transition-delay: 0ms;
  }

  .content {
    overflow: hidden;
    display: flex;
    flex-direction: row;
    gap: 16px;
    height: 24px;
    opacity: 1;
    padding: 20px 40px;
    transition: all 300ms ease-in-out;
  }

  .content.collapsed {
    height: 0;
    opacity: 0;
    padding: 0px 40px;
    transition-delay: 0ms;
  }

  .content.expanded {
    height: 24px;
    opacity: 1;
    transition-delay: 150ms;
  }

  .content p {
    margin: 0;
  }

  .slider {
    position: absolute;
    left: calc(var(--left) + 6px);
    right: va(--left);
    bottom: 6px;
    height: 40px;
    width: calc(var(--right) - var(--left) - 12px);
    border-radius: 12px;
    background-color: #87b5eb70;
    transition: left 300ms ease-in-out;
  }

  .items {
    display: flex;
    flex-direction: row;
    justify-content: space-around;
    height: 40px;
  }

  /* set the icon color	 */
  .item {
    color: black;
  }

  /* select a better default font */
  * {
    font-family: Arial, Helvetica, sans-serif;
    margin: 0;
    padding: 0;
  }
</style>

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