diff --git a/ui/src/components/AlbumFilter.tsx b/ui/src/components/AlbumFilter.tsx
deleted file mode 100644
index d391cde..0000000
--- a/ui/src/components/AlbumFilter.tsx
+++ /dev/null
@@ -1,104 +0,0 @@
-import React from 'react'
-import { authToken } from '../helpers/authentication'
-import { useTranslation } from 'react-i18next'
-import { OrderDirection } from '../../__generated__/globalTypes'
-import { MediaOrdering, SetOrderingFn } from '../hooks/useOrderingParams'
-
-type FavoriteCheckboxProps = {
-  onlyFavorites: boolean
-  setOnlyFavorites(favorites: boolean): void
-}
-
-export const FavoritesCheckbox = ({
-  onlyFavorites,
-  setOnlyFavorites,
-}: FavoriteCheckboxProps) => {
-  const { t } = useTranslation()
-
-  return (
-    <label>
-      <input
-        type="checkbox"
-        checked={onlyFavorites}
-        onChange={e => setOnlyFavorites(e.target.checked)}
-      />
-      <span>{t('album_filter.only_favorites', 'Show only favorites')}</span>
-    </label>
-  )
-}
-
-type AlbumFilterProps = {
-  onlyFavorites: boolean
-  setOnlyFavorites?(favorites: boolean): void
-  ordering?: MediaOrdering
-  setOrdering?: SetOrderingFn
-}
-
-const AlbumFilter = ({
-  onlyFavorites,
-  setOnlyFavorites,
-  setOrdering,
-  ordering,
-}: AlbumFilterProps) => {
-  const { t } = useTranslation()
-
-  const changeOrderDirection = () => {
-    if (setOrdering && ordering) {
-      setOrdering({
-        orderDirection:
-          ordering.orderDirection == OrderDirection.ASC
-            ? OrderDirection.DESC
-            : OrderDirection.ASC,
-      })
-    }
-  }
-
-  const changeOrderBy = (e: React.ChangeEvent<HTMLSelectElement>) => {
-    if (setOrdering) {
-      setOrdering({ orderBy: e.target.value })
-    }
-  }
-
-  const sortingOptions = [
-    {
-      value: 'date_shot',
-      text: t('album_filter.sorting_options.date_shot', 'Date shot'),
-    },
-    {
-      value: 'updated_at',
-      text: t('album_filter.sorting_options.date_imported', 'Date imported'),
-    },
-    {
-      value: 'title',
-      text: t('album_filter.sorting_options.title', 'Title'),
-    },
-    {
-      value: 'type',
-      text: t('album_filter.sorting_options.type', 'Kind'),
-    },
-  ]
-
-  return (
-    <>
-      {authToken() && setOnlyFavorites && (
-        <FavoritesCheckbox
-          onlyFavorites={onlyFavorites}
-          setOnlyFavorites={setOnlyFavorites}
-        />
-      )}
-      <span>{t('album_filter.sort_by', 'Sort by')}</span>
-
-      <select onChange={changeOrderBy} value={ordering?.orderBy || undefined}>
-        {sortingOptions.map(x => (
-          <option key={x.value} value={x.value}>
-            {x.text}
-          </option>
-        ))}
-      </select>
-
-      <button onClick={changeOrderDirection}>{ordering?.orderDirection}</button>
-    </>
-  )
-}
-
-export default AlbumFilter
diff --git a/ui/src/components/album/AlbumFilter.tsx b/ui/src/components/album/AlbumFilter.tsx
new file mode 100644
index 0000000..509115c
--- /dev/null
+++ b/ui/src/components/album/AlbumFilter.tsx
@@ -0,0 +1,133 @@
+import React from 'react'
+import { authToken } from '../../helpers/authentication'
+import { useTranslation } from 'react-i18next'
+import { OrderDirection } from '../../../__generated__/globalTypes'
+import { MediaOrdering, SetOrderingFn } from '../../hooks/useOrderingParams'
+import Checkbox from '../../primitives/form/Checkbox'
+
+import { ReactComponent as SortingIcon } from './icons/sorting.svg'
+import { ReactComponent as DirectionIcon } from './icons/direction-arrow.svg'
+
+import Dropdown from '../../primitives/form/Dropdown'
+
+type FavoriteCheckboxProps = {
+  onlyFavorites: boolean
+  setOnlyFavorites(favorites: boolean): void
+}
+
+export const FavoritesCheckbox = ({
+  onlyFavorites,
+  setOnlyFavorites,
+}: FavoriteCheckboxProps) => {
+  const { t } = useTranslation()
+
+  return (
+    <Checkbox
+      className="mb-1"
+      label={t('album_filter.only_favorites', 'Show only favorites')}
+      checked={onlyFavorites}
+      onChange={e => setOnlyFavorites(e.target.checked)}
+    />
+  )
+}
+
+type SortingOptionsProps = {
+  ordering?: MediaOrdering
+  setOrdering?: SetOrderingFn
+}
+
+const SortingOptions = ({ setOrdering, ordering }: SortingOptionsProps) => {
+  const { t } = useTranslation()
+
+  const changeOrderDirection = () => {
+    if (setOrdering && ordering) {
+      setOrdering({
+        orderDirection:
+          ordering.orderDirection == OrderDirection.ASC
+            ? OrderDirection.DESC
+            : OrderDirection.ASC,
+      })
+    }
+  }
+
+  const changeOrderBy = (value: string) => {
+    if (setOrdering) {
+      setOrdering({ orderBy: value })
+    }
+  }
+
+  const sortingOptions = [
+    {
+      value: 'date_shot',
+      label: t('album_filter.sorting_options.date_shot', 'Date shot'),
+    },
+    {
+      value: 'updated_at',
+      label: t('album_filter.sorting_options.date_imported', 'Date imported'),
+    },
+    {
+      value: 'title',
+      label: t('album_filter.sorting_options.title', 'Title'),
+    },
+    {
+      value: 'type',
+      label: t('album_filter.sorting_options.type', 'Kind'),
+    },
+  ]
+
+  return (
+    <fieldset>
+      <legend id="filter_group_sort-label" className="inline-block mb-1">
+        <SortingIcon
+          className="inline-block align-baseline mr-1"
+          aria-hidden="true"
+        />
+        <span>{t('album_filter.sort', 'Sort')}</span>
+      </legend>
+      <div>
+        <Dropdown
+          aria-labelledby="filter_group_sort-label"
+          setSelected={changeOrderBy}
+          value={ordering?.orderBy || undefined}
+          items={sortingOptions}
+        />
+        <button
+          className={`bg-gray-50 h-[30px] align-top px-2 py-1 rounded ml-2 border border-gray-200 focus:outline-none focus:border-blue-300 text-[#8b8b8b] hover:bg-gray-100 hover:text-[#777] ${
+            ordering?.orderDirection == OrderDirection.ASC ? 'flip-y' : null
+          }`}
+          onClick={changeOrderDirection}
+        >
+          <DirectionIcon />
+        </button>
+      </div>
+    </fieldset>
+  )
+}
+
+type AlbumFilterProps = {
+  onlyFavorites: boolean
+  setOnlyFavorites?(favorites: boolean): void
+  ordering?: MediaOrdering
+  setOrdering?: SetOrderingFn
+}
+
+const AlbumFilter = ({
+  onlyFavorites,
+  setOnlyFavorites,
+  setOrdering,
+  ordering,
+}: AlbumFilterProps) => {
+  return (
+    <div className="flex items-end gap-4 flex-wrap mb-4">
+      <SortingOptions ordering={ordering} setOrdering={setOrdering} />
+      {authToken() && setOnlyFavorites && (
+        <FavoritesCheckbox
+          onlyFavorites={onlyFavorites}
+          setOnlyFavorites={setOnlyFavorites}
+        />
+      )}
+    </div>
+  )
+}
+
+export default AlbumFilter
diff --git a/ui/src/components/AlbumTitle.tsx b/ui/src/components/album/AlbumTitle.tsx
similarity index 65%
rename from ui/src/components/AlbumTitle.tsx
rename to ui/src/components/album/AlbumTitle.tsx
index 5c6fd7a..76b989a 100644
--- a/ui/src/components/AlbumTitle.tsx
+++ b/ui/src/components/album/AlbumTitle.tsx
@@ -1,11 +1,14 @@
 import React, { useEffect, useContext } from 'react'
 import { Link } from 'react-router-dom'
 import styled from 'styled-components'
-import { SidebarContext } from './sidebar/Sidebar'
-import AlbumSidebar from './sidebar/AlbumSidebar'
+import { SidebarContext } from '../sidebar/Sidebar'
+import AlbumSidebar from '../sidebar/AlbumSidebar'
 import { useLazyQuery, gql } from '@apollo/client'
-import { authToken } from '../helpers/authentication'
+import { authToken } from '../../helpers/authentication'
 import { albumPathQuery } from './__generated__/albumPathQuery'
+import useDelay from '../../hooks/useDelay'
+
+import { ReactComponent as GearIcon } from './icons/gear.svg'
 
 const BreadcrumbList = styled.ol`
   & li::after {
@@ -56,7 +59,21 @@ const AlbumTitle = ({ album, disableLink = false }: AlbumTitleProps) => {
     }
   }, [album])
 
-  if (!album) return <div style={{ height: 36 }}></div>
+  const delay = useDelay(200, [album])
+  console.log('delay', delay)
+
+  if (!album) {
+    return (
+      <div
+        className={`flex mb-6 flex-col h-14 transition-opacity animate-pulse ${
+          delay ? 'opacity-100' : 'opacity-0'
+        }`}
+      >
+        <div className="w-32 h-4 bg-gray-100 mb-2 mt-1"></div>
+        <div className="w-72 h-6 bg-gray-100"></div>
+      </div>
+    )
+  }
 
   let title = <span>{album.title}</span>
 
@@ -76,20 +93,21 @@ const AlbumTitle = ({ album, disableLink = false }: AlbumTitleProps) => {
   }
 
   return (
-    <div className="flex">
-      <div>
+    <div className="flex mb-6 items-end h-14">
+      <div className="min-w-0">
         <nav aria-label="Album breadcrumb">
-          <BreadcrumbList className="">{breadcrumbSections}</BreadcrumbList>
+          <BreadcrumbList>{breadcrumbSections}</BreadcrumbList>
         </nav>
-        <h1 className="text-2xl">{title}</h1>
+        <h1 className="text-2xl truncate min-w-0">{title}</h1>
       </div>
       {authToken() && (
         <button
+          className="bg-gray-50 p-2 rounded ml-2 border border-gray-200 focus:outline-none focus:border-blue-300 text-[#8b8b8b] hover:bg-gray-100 hover:text-[#777]"
           onClick={() => {
             updateSidebar(<AlbumSidebar albumId={album.id} />)
           }}
         >
-          More
+          <GearIcon />
         </button>
       )}
     </div>
diff --git a/ui/src/components/album/__generated__/albumPathQuery.ts b/ui/src/components/album/__generated__/albumPathQuery.ts
new file mode 100644
index 0000000..6d141f5
--- /dev/null
+++ b/ui/src/components/album/__generated__/albumPathQuery.ts
@@ -0,0 +1,32 @@
+/* tslint:disable */
+/* eslint-disable */
+// @generated
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL query operation: albumPathQuery
+// ====================================================
+
+export interface albumPathQuery_album_path {
+  __typename: 'Album'
+  id: string
+  title: string
+}
+
+export interface albumPathQuery_album {
+  __typename: 'Album'
+  id: string
+  path: albumPathQuery_album_path[]
+}
+
+export interface albumPathQuery {
+  /**
+   * Get album by id, user must own the album or be admin
+   * If valid tokenCredentials are provided, the album may be retrived without further authentication
+   */
+  album: albumPathQuery_album
+}
+
+export interface albumPathQueryVariables {
+  id: string
+}
diff --git a/ui/src/components/album/icons/direction-arrow.svg b/ui/src/components/album/icons/direction-arrow.svg
new file mode 100644
index 0000000..1860eff
--- /dev/null
+++ b/ui/src/components/album/icons/direction-arrow.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="13px" height="8px" viewBox="0 0 13 8" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+  <polyline fill="none" stroke="currentColor" stroke-width="1.5" points="1 7 6.5 1 12 7"></polyline>
+</svg>
\ No newline at end of file
diff --git a/ui/src/components/album/icons/gear.svg b/ui/src/components/album/icons/gear.svg
new file mode 100644
index 0000000..8e48f3f
--- /dev/null
+++ b/ui/src/components/album/icons/gear.svg
@@ -0,0 +1,7 @@
+<svg
+  viewBox="0 0 15 15"
+  width="15px"
+  height="15px"
+  fill="currentColor">
+  <path d="M7.33333333,0 C8.069713,0 8.66666667,0.596953667 8.66666667,1.33333333 L8.66666667,1.39333333 C8.66841527,1.83176195 8.93035293,2.22728781 9.33333333,2.4 C9.74472671,2.5815643 10.2252088,2.49444392 10.5466667,2.18 L10.5866667,2.14 C10.8367577,1.88963061 11.176121,1.74895112 11.53,1.74895112 C11.883879,1.74895112 12.2232423,1.88963061 12.4733333,2.14 C12.7237027,2.39009101 12.8643822,2.72945434 12.8643822,3.08333333 C12.8643822,3.43721233 12.7237027,3.77657566 12.4733333,4.02666667 L12.4333333,4.06666667 C12.1188894,4.38812449 12.031769,4.86860662 12.2133333,5.28 L12.2133333,5.33333333 C12.3860455,5.73631374 12.7815714,5.9982514 13.22,6 L13.3333333,6 C14.069713,6 14.6666667,6.59695367 14.6666667,7.33333333 C14.6666667,8.069713 14.069713,8.66666667 13.3333333,8.66666667 L13.2733333,8.66666667 C12.8349047,8.66841527 12.4393789,8.93035293 12.2666667,9.33333333 C12.0851024,9.74472671 12.1722227,10.2252088 12.4866667,10.5466667 L12.5266667,10.5866667 C12.7770361,10.8367577 12.9177155,11.176121 12.9177155,11.53 C12.9177155,11.883879 12.7770361,12.2232423 12.5266667,12.4733333 C12.2765757,12.7237027 11.9372123,12.8643822 11.5833333,12.8643822 C11.2294543,12.8643822 10.890091,12.7237027 10.64,12.4733333 L10.6,12.4333333 C10.2785422,12.1188894 9.79806005,12.031769 9.38666667,12.2133333 C8.98368626,12.3860455 8.7217486,12.7815714 8.72,13.22 L8.72,13.3333333 C8.72,14.069713 8.12304633,14.6666667 7.38666667,14.6666667 C6.650287,14.6666667 6.05333333,14.069713 6.05333333,13.3333333 L6.05333333,13.2733333 C6.04277107,12.8217805 5.75724785,12.4225768 5.33333333,12.2666667 C4.92193995,12.0851024 4.44145782,12.1722227 4.12,12.4866667 L4.08,12.5266667 C3.82990899,12.7770361 3.49054566,12.9177155 3.13666667,12.9177155 C2.78278767,12.9177155 2.44342434,12.7770361 2.19333333,12.5266667 C1.94296394,12.2765757 1.80228446,11.9372123 1.80228446,11.5833333 C1.80228446,11.2294543 1.94296394,10.890091 2.19333333,10.64 L2.23333333,10.6 C2.54777725,10.2785422 2.63489764,9.79806005 2.45333333,9.38666667 C2.28062114,8.98368626 1.88509528,8.7217486 1.44666667,8.72 L1.33333333,8.72 C0.596953667,8.72 0,8.12304633 0,7.38666667 C0,6.650287 0.596953667,6.05333333 1.33333333,6.05333333 L1.39333333,6.05333333 C1.84488612,6.04277107 2.24408988,5.75724785 2.4,5.33333333 C2.5815643,4.92193995 2.49444392,4.44145782 2.18,4.12 L2.14,4.08 C1.88963061,3.82990899 1.74895112,3.49054566 1.74895112,3.13666667 C1.74895112,2.78278767 1.88963061,2.44342434 2.14,2.19333333 C2.39009101,1.94296394 2.72945434,1.80228446 3.08333333,1.80228446 C3.43721233,1.80228446 3.77657566,1.94296394 4.02666667,2.19333333 L4.06666667,2.23333333 C4.38812449,2.54777725 4.86860662,2.63489764 5.28,2.45333333 L5.33333333,2.45333333 C5.73631374,2.28062114 5.9982514,1.88509528 6,1.44666667 L6,1.33333333 C6,0.596953667 6.59695367,0 7.33333333,0 Z M7.33333333,5.33333333 C6.22876383,5.33333333 5.33333333,6.22876383 5.33333333,7.33333333 C5.33333333,8.43790283 6.22876383,9.33333333 7.33333333,9.33333333 C8.43790283,9.33333333 9.33333333,8.43790283 9.33333333,7.33333333 C9.33333333,6.22876383 8.43790283,5.33333333 7.33333333,5.33333333 Z" />
+</svg>
\ No newline at end of file
diff --git a/ui/src/components/album/icons/sorting.svg b/ui/src/components/album/icons/sorting.svg
new file mode 100644
index 0000000..e2ee5c8
--- /dev/null
+++ b/ui/src/components/album/icons/sorting.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="15px" height="13px" viewBox="0 0 15 13" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+  <path fill="currentColor" d="M4,3.55271368e-14 L4.0278401,0.000770874105 C4.04128052,0.00151580687 4.05469661,0.00280063794 4.06805308,0.0046253673 L4,3.55271368e-14 C4.04183471,3.55271368e-14 4.08246942,0.00513782494 4.12130755,0.0148169071 C4.13326871,0.0178373169 4.14523456,0.0212920805 4.15708315,0.0252017355 C4.16997104,0.0294299553 4.18277427,0.034259294 4.19531518,0.0395860503 C4.20839103,0.0451160612 4.22150919,0.0513529918 4.23439118,0.0581928845 C4.23503401,0.0585723692 4.23566876,0.058911632 4.23630269,0.0592522225 L4.30644972,0.104852306 L4.30644972,0.104852306 L4.35355339,0.146446609 L7.85355339,3.64644661 C8.04881554,3.84170876 8.04881554,4.15829124 7.85355339,4.35355339 C7.67331141,4.53379537 7.38969588,4.54766014 7.19355028,4.39514769 L7.14644661,4.35355339 L4.5,1.707 L4.5,12 C4.5,12.2761424 4.27614237,12.5 4,12.5 C3.74358208,12.5 3.53224642,12.3069799 3.50336387,12.0583106 L3.5,12 L3.5,1.707 L0.853553391,4.35355339 C0.67331141,4.53379537 0.389695882,4.54766014 0.193550278,4.39514769 L0.146446609,4.35355339 C-0.0337953714,4.17331141 -0.0476601392,3.88969588 0.104852306,3.69355028 L0.146446609,3.64644661 L3.64644661,0.146446609 L3.65767597,0.135561002 C3.67134412,0.122717283 3.68573494,0.110633776 3.70078091,0.0993780001 L3.64644661,0.146446609 C3.67320128,0.11969194 3.70223365,0.0966031755 3.73291954,0.0771803148 C3.74617456,0.0688441438 3.75977662,0.0610999945 3.77375753,0.0539938991 C3.78609769,0.0476919126 3.79888567,0.0418687101 3.81184952,0.0366116524 C3.8234432,0.0319023624 3.83504722,0.0276848515 3.84684229,0.0238934918 C3.86557052,0.0179217879 3.88445336,0.0130756914 3.90351142,0.00934593934 C3.91016306,0.00799330119 3.91744282,0.00672777028 3.92477476,0.00562145489 C3.94767442,0.00222086491 3.97013713,0.000384038311 3.99261784,5.41592491e-05 L4,3.55271368e-14 Z M11,0 C11.2564179,0 11.4677536,0.193020095 11.4966361,0.441689437 L11.5,0.5 L11.499,10.792 L14.1464466,8.14644661 C14.3266886,7.96620463 14.6103041,7.95233986 14.8064497,8.10485231 L14.8535534,8.14644661 C15.0337954,8.32668859 15.0476601,8.61030412 14.8951477,8.80644972 L14.8535534,8.85355339 L11.3535534,12.3535534 L11.342324,12.364439 C11.3286559,12.3772827 11.3142651,12.3893662 11.2992191,12.400622 L11.3535534,12.3535534 C11.3267987,12.3803081 11.2977664,12.4033968 11.2670805,12.4228197 C11.2538254,12.4311559 11.2402234,12.4389 11.2262425,12.4460061 C11.2139023,12.4523081 11.2011143,12.4581313 11.1881505,12.4633883 C11.1765568,12.4680976 11.1649528,12.4723151 11.1531577,12.4761065 C11.1344295,12.4820782 11.1155466,12.4869243 11.0964886,12.4906541 C11.08887,12.4921969 11.0806073,12.4936082 11.0722781,12.4948142 C11.0526434,12.4976054 11.0335482,12.4992446 11.0144276,12.4997931 L11,12.5 L11,12.5 L10.9590982,12.4983345 C10.9507048,12.4976497 10.9423254,12.4967538 10.9339686,12.4956467 L11,12.5 C10.9581653,12.5 10.9175306,12.4948622 10.8786924,12.4851831 C10.8667313,12.4821627 10.8547654,12.4787079 10.8429168,12.4747983 C10.828989,12.4702266 10.8151853,12.4649672 10.8016896,12.4591301 C10.7896647,12.4539556 10.7775355,12.4481398 10.7656088,12.4418071 C10.74145,12.4289242 10.7184764,12.4141602 10.6968848,12.3976777 L10.6935503,12.3951477 L10.6464466,12.3535534 L7.14644661,8.85355339 C6.95118446,8.65829124 6.95118446,8.34170876 7.14644661,8.14644661 C7.32668859,7.96620463 7.61030412,7.95233986 7.80644972,8.10485231 L7.85355339,8.14644661 L10.499,10.792 L10.5,0.5 C10.5,0.223857625 10.7238576,0 11,0 Z"></path>
+</svg>
\ No newline at end of file
diff --git a/ui/src/components/layout/__generated__/adminQuery.ts b/ui/src/components/layout/__generated__/adminQuery.ts
new file mode 100644
index 0000000..2301f23
--- /dev/null
+++ b/ui/src/components/layout/__generated__/adminQuery.ts
@@ -0,0 +1,20 @@
+/* tslint:disable */
+/* eslint-disable */
+// @generated
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL query operation: adminQuery
+// ====================================================
+
+export interface adminQuery_myUser {
+  __typename: 'User'
+  admin: boolean
+}
+
+export interface adminQuery {
+  /**
+   * Information about the currently logged in user
+   */
+  myUser: adminQuery_myUser
+}
diff --git a/ui/src/components/layout/__generated__/mapboxEnabledQuery.ts b/ui/src/components/layout/__generated__/mapboxEnabledQuery.ts
new file mode 100644
index 0000000..0600784
--- /dev/null
+++ b/ui/src/components/layout/__generated__/mapboxEnabledQuery.ts
@@ -0,0 +1,15 @@
+/* tslint:disable */
+/* eslint-disable */
+// @generated
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL query operation: mapboxEnabledQuery
+// ====================================================
+
+export interface mapboxEnabledQuery {
+  /**
+   * Get the mapbox api token, returns null if mapbox is not enabled
+   */
+  mapboxToken: string | null
+}
diff --git a/ui/src/components/layout/menu-icons/albums.svg b/ui/src/components/layout/menu-icons/albums.svg
new file mode 100644
index 0000000..1e93f5e
--- /dev/null
+++ b/ui/src/components/layout/menu-icons/albums.svg
@@ -0,0 +1,5 @@
+<svg viewBox="0 0 24 24" fill="white">
+  <path
+    d="M6,16 L19,16 C19.5522847,16 20,16.4477153 20,17 L20,21 C20,21.5522847 19.5522847,22 19,22 L6,22 C4.8954305,22 4,21.1045695 4,20 L4,18 C4,16.8954305 4.8954305,16 6,16 Z" fillOpacity="0.75" />
+  <path d="M19,2 C19.5522847,2 20,2.44771525 20,3 L20,19 C20,19.5522847 19.5522847,20 19,20 L6,20 C4.8954305,20 4,19.1045695 4,18 L4,4 C4,2.8954305 4.8954305,2 6,2 L19,2 Z M14.4676845,9 L11.5079767,12.9536523 L9.50029382,10.8745763 L7,14 L18,14 L14.4676845,9 Z M10.75,9 C10.3357864,9 10,9.33578644 10,9.75 C10,10.1642136 10.3357864,10.5 10.75,10.5 C11.1642136,10.5 11.5,10.1642136 11.5,9.75 C11.5,9.33578644 11.1642136,9 10.75,9 Z" />
+</svg>
\ No newline at end of file
diff --git a/ui/src/components/layout/menu-icons/people.svg b/ui/src/components/layout/menu-icons/people.svg
new file mode 100644
index 0000000..2af9060
--- /dev/null
+++ b/ui/src/components/layout/menu-icons/people.svg
@@ -0,0 +1,7 @@
+<svg viewBox="0 0 24 24" fill="white">
+  <path
+    d="M12,15 C15.1826579,15 18.0180525,16.4868108 19.8494955,18.8037439 L20,19 C20,20.6568542 18.6568542,22 17,22 L7,22 C5.34314575,22 4,20.6568542 4,19 L4.15050454,18.8037439 C5.9819475,16.4868108 8.81734212,15 12,15 Z"
+    fillOpacity="0.75"
+  ></path>
+  <circle cx="12" cy="11" r="6"></circle>
+</svg>
\ No newline at end of file
diff --git a/ui/src/components/layout/menu-icons/photos.svg b/ui/src/components/layout/menu-icons/photos.svg
new file mode 100644
index 0000000..5b801f1
--- /dev/null
+++ b/ui/src/components/layout/menu-icons/photos.svg
@@ -0,0 +1,4 @@
+<svg viewBox="0 0 24 24" fill="white">
+  <path d="M15.6971052,10 L23.9367603,21.526562 C23.6878671,22.9323278 22.4600272,24 20.982819,24 L5.851819,24 L15.6971052,10 Z" />
+  <path d="M5.59307375,14 L15.562,24 L2.982819,24 C1.43507633,24 0.161084327,22.8279341 -3.56225466e-14,21.3229592 L5.59307375,14 Z" fillOpacity="0.75"/>
+</svg>
\ No newline at end of file
diff --git a/ui/src/components/layout/menu-icons/places.svg b/ui/src/components/layout/menu-icons/places.svg
new file mode 100644
index 0000000..3032ea3
--- /dev/null
+++ b/ui/src/components/layout/menu-icons/places.svg
@@ -0,0 +1,8 @@
+<svg viewBox="0 0 24 24" fill="white">
+  <path d="M3.4,3.34740684 C3.47896999,3.34740684 3.55617307,3.37078205 3.62188008,3.41458672 L9,7 L9,21 L3.4452998,17.2968665 C3.16710114,17.1114008 3,16.7991694 3,16.4648162 L3,3.74740684 C3,3.52649294 3.1790861,3.34740684 3.4,3.34740684 Z M15,3 L21.4961389,6.71207939 C21.8077139,6.89012225 22,7.22146569 22,7.58032254 L22,20.3107281 C22,20.531642 21.8209139,20.7107281 21.6,20.7107281 C21.5303892,20.7107281 21.4619835,20.692562 21.4015444,20.6580254 L15,17 L15,3 Z" />
+  <polygon
+    fillOpacity="0.75"
+    transform="translate(12, 12) scale(1, -1) translate(-12, -12)"
+    points="9 3 15 7 15 21 9 17"
+  />
+</svg>
\ No newline at end of file
diff --git a/ui/src/components/layout/menu-icons/settings.svg b/ui/src/components/layout/menu-icons/settings.svg
new file mode 100644
index 0000000..709a765
--- /dev/null
+++ b/ui/src/components/layout/menu-icons/settings.svg
@@ -0,0 +1,7 @@
+<svg viewBox="0 0 24 24" fill="white">
+  <path
+    d="M13.4691754,16.7806702 L13,21 L11,21 L10.5318353,16.7809803 C10.9960818,16.9233714 11.4890921,17 12,17 C12.5112786,17 13.0046337,16.9232601 13.4691754,16.7806702 Z M16.4192861,14.3409153 L19.0710678,17.6568542 L17.6568542,19.0710678 L14.3409153,16.4192861 C15.2243208,15.9503691 15.9503691,15.2243208 16.4192861,14.3409153 Z M9.65908474,16.4192861 L6.34314575,19.0710678 L4.92893219,17.6568542 L7.5807139,14.3409153 C8.04963086,15.2243208 8.77567918,15.9503691 9.65908474,16.4192861 Z M7,12 C7,12.5112786 7.07673988,13.0046337 7.21932976,13.4691754 L3,13 L3,11 L7.21901966,10.5318353 C7.07662862,10.9960818 7,11.4890921 7,12 Z M16.7809803,10.5318353 L21,11 L21,13 L16.7806702,13.4691754 C16.9232601,13.0046337 17,12.5112786 17,12 C17,11.4890921 16.9233714,10.9960818 16.7809803,10.5318353 Z M6.34314575,4.92893219 L9.65908474,7.5807139 C8.77567918,8.04963086 8.04963086,8.77567918 7.5807139,9.65908474 L4.92893219,6.34314575 L6.34314575,4.92893219 Z M17.6568542,4.92893219 L19.0710678,6.34314575 L16.4192861,9.65908474 C15.9503691,8.77567918 15.2243208,8.04963086 14.3409153,7.5807139 L17.6568542,4.92893219 Z M13,3 L13.4691754,7.21932976 C13.0046337,7.07673988 12.5112786,7 12,7 C11.4890921,7 10.9960818,7.07662862 10.5318353,7.21901966 L11,3 L13,3 Z"
+    fillOpacity="0.76"
+  />
+  <path d="M12,5 C15.8659932,5 19,8.13400675 19,12 C19,15.8659932 15.8659932,19 12,19 C8.13400675,19 5,15.8659932 5,12 C5,8.13400675 8.13400675,5 12,5 Z M12,8 C9.790861,8 8,9.790861 8,12 C8,14.209139 9.790861,16 12,16 C14.209139,16 16,14.209139 16,12 C16,9.790861 14.209139,8 12,8 Z" />
+</svg>
\ No newline at end of file
diff --git a/ui/src/hooks/useDelay.ts b/ui/src/hooks/useDelay.ts
new file mode 100644
index 0000000..5a45309
--- /dev/null
+++ b/ui/src/hooks/useDelay.ts
@@ -0,0 +1,23 @@
+import { useLayoutEffect, useState, useRef } from 'react'
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function useDelay(wait: number, deps: any[] = []) {
+  const [_, triggerUpdate] = useState(false)
+  const done = useRef(false)
+
+  useLayoutEffect(() => {
+    const handle = setTimeout(() => {
+      done.current = true
+      triggerUpdate(x => !x)
+    }, wait)
+
+    return () => {
+      done.current = false
+      clearTimeout(handle)
+    }
+  }, deps)
+
+  return done.current
+}
+
+export default useDelay
diff --git a/ui/src/primitives/form/Checkbox.tsx b/ui/src/primitives/form/Checkbox.tsx
new file mode 100644
index 0000000..a2b1268
--- /dev/null
+++ b/ui/src/primitives/form/Checkbox.tsx
@@ -0,0 +1,54 @@
+import React from 'react'
+import styled from 'styled-components'
+
+const CheckboxLabelWrapper = styled.label`
+  & input[type='checkbox'] {
+    appearance: none;
+    outline: none;
+  }
+
+  & input[type='checkbox'] + span {
+    cursor: pointer;
+  }
+
+  & input[type='checkbox'] + span::before {
+    content: '';
+    background-repeat: no-repeat;
+    background-position: center;
+    width: 12px;
+    height: 12px;
+    background-color: #f9fafb;
+    border: 1px solid #aaa;
+    display: inline-block;
+    border-radius: 3px;
+    cursor: pointer;
+    margin-right: 8px;
+    margin-bottom: -1px;
+  }
+
+  & input[type='checkbox']:checked + span::before {
+    background-color: #0072ff;
+    border-color: #109dff;
+    background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='7px' height='4px' viewBox='0 0 7 4' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3EPath 5%3C/title%3E%3Cg id='Page-1' stroke='none' stroke-width='1.5' fill='none' fill-rule='evenodd'%3E%3Cpolyline id='Path-5' stroke='%23FFFFFF' points='1 1.93487039 2.77174339 3.70661378 6.47835718 2.27373675e-13'%3E%3C/polyline%3E%3C/g%3E%3C/svg%3E");
+  }
+
+  & input[type='checkbox']:focus + span::before {
+    outline: 2px solid #86c6ff;
+    border-color: #109dff;
+  }
+`
+
+type CheckboxProps = React.InputHTMLAttributes<HTMLInputElement> & {
+  label: string
+}
+
+const Checkbox = ({ label, className, ...props }: CheckboxProps) => {
+  return (
+    <CheckboxLabelWrapper className={className}>
+      <input type="checkbox" {...props} />
+      <span>{label}</span>
+    </CheckboxLabelWrapper>
+  )
+}
+
+export default Checkbox
diff --git a/ui/src/primitives/form/Dropdown.tsx b/ui/src/primitives/form/Dropdown.tsx
new file mode 100644
index 0000000..f69e22a
--- /dev/null
+++ b/ui/src/primitives/form/Dropdown.tsx
@@ -0,0 +1,52 @@
+import React from 'react'
+import styled from 'styled-components'
+
+const DropdownStyledSelect = styled.select`
+  appearance: none;
+
+  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='9px' height='5px' viewBox='0 0 9 5'%3E%3Cpolygon fill='%23D8D8D8' points='0 0 8.36137659 0 4.1806883 4.1806883'%3E%3C/polygon%3E%3C/svg%3E");
+  background-repeat: no-repeat;
+  background-position: center right 10px;
+`
+
+type DropdownItem = {
+  value: string
+  label: string
+}
+
+type DropdownProps = React.SelectHTMLAttributes<HTMLSelectElement> & {
+  items: DropdownItem[]
+  selected?: string
+  setSelected(label: string): void
+}
+
+const Dropdown = ({
+  items,
+  selected,
+  setSelected,
+  ...otherProps
+}: DropdownProps) => {
+  const onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
+    setSelected(e.target.value)
+    otherProps.onChange && otherProps.onChange(e)
+  }
+
+  const options = items.map(({ value, label }) => (
+    <option key={value} value={value}>
+      {label}
+    </option>
+  ))
+
+  return (
+    <DropdownStyledSelect
+      className="bg-gray-50 px-2 py-0.5 pr-6 rounded border border-gray-200 focus:outline-none focus:border-blue-300 text-[#222] hover:bg-gray-100"
+      value={selected}
+      onChange={onChange}
+      {...otherProps}
+    >
+      {options}
+    </DropdownStyledSelect>
+  )
+}
+
+export default Dropdown
