diff --git a/i18n/en.json b/i18n/en.json
index 8d1cc3a2b3..de17cccebd 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -864,6 +864,7 @@
   "loop_videos": "Loop videos",
   "loop_videos_description": "Enable to automatically loop a video in the detail viewer.",
   "main_branch_warning": "You’re using a development version; we strongly recommend using a release version!",
+  "main_menu": "Main menu",
   "make": "Make",
   "manage_shared_links": "Manage shared links",
   "manage_sharing_with_partners": "Manage sharing with partners",
diff --git a/web/src/lib/actions/__test__/focus-trap-test.svelte b/web/src/lib/actions/__test__/focus-trap-test.svelte
index e1cb6fa4fb..a19d2b75db 100644
--- a/web/src/lib/actions/__test__/focus-trap-test.svelte
+++ b/web/src/lib/actions/__test__/focus-trap-test.svelte
@@ -3,15 +3,16 @@
 
   interface Props {
     show: boolean;
+    active?: boolean;
   }
 
-  let { show = $bindable() }: Props = $props();
+  let { show = $bindable(), active = $bindable() }: Props = $props();
 </script>
 
 <button type="button" onclick={() => (show = true)}>Open</button>
 
 {#if show}
-  <div use:focusTrap>
+  <div use:focusTrap={{ active }}>
     <div>
       <span>text</span>
       <button data-testid="one" type="button" onclick={() => (show = false)}>Close</button>
diff --git a/web/src/lib/actions/__test__/focus-trap.spec.ts b/web/src/lib/actions/__test__/focus-trap.spec.ts
index 6ce5ad6d5b..d92d8e037d 100644
--- a/web/src/lib/actions/__test__/focus-trap.spec.ts
+++ b/web/src/lib/actions/__test__/focus-trap.spec.ts
@@ -12,6 +12,12 @@ describe('focusTrap action', () => {
     expect(document.activeElement).toEqual(screen.getByTestId('one'));
   });
 
+  it('should not set focus if inactive', async () => {
+    render(FocusTrapTest, { show: true, active: false });
+    await tick();
+    expect(document.activeElement).toBe(document.body);
+  });
+
   it('supports backward focus wrapping', async () => {
     render(FocusTrapTest, { show: true });
     await tick();
diff --git a/web/src/lib/actions/click-outside.ts b/web/src/lib/actions/click-outside.ts
index 92775546aa..599a97af75 100644
--- a/web/src/lib/actions/click-outside.ts
+++ b/web/src/lib/actions/click-outside.ts
@@ -35,12 +35,12 @@ export function clickOutside(node: HTMLElement, options: Options = {}): ActionRe
     }
   };
 
-  document.addEventListener('mousedown', handleClick, true);
+  document.addEventListener('mousedown', handleClick, false);
   node.addEventListener('keydown', handleKey, false);
 
   return {
     destroy() {
-      document.removeEventListener('mousedown', handleClick, true);
+      document.removeEventListener('mousedown', handleClick, false);
       node.removeEventListener('keydown', handleKey, false);
     },
   };
diff --git a/web/src/lib/actions/focus-trap.ts b/web/src/lib/actions/focus-trap.ts
index 7483e76099..1a84f21729 100644
--- a/web/src/lib/actions/focus-trap.ts
+++ b/web/src/lib/actions/focus-trap.ts
@@ -1,16 +1,34 @@
 import { shortcuts } from '$lib/actions/shortcut';
 import { tick } from 'svelte';
 
-const selectors =
-  'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
+interface Options {
+  /**
+   * Set whether the trap is active or not.
+   */
+  active?: boolean;
+}
 
-export function focusTrap(container: HTMLElement) {
+const selectors =
+  'button:not([disabled], .hidden), [href]:not(.hidden), input:not([disabled], .hidden), select:not([disabled], .hidden), textarea:not([disabled], .hidden), [tabindex]:not([tabindex="-1"], .hidden)';
+
+export function focusTrap(container: HTMLElement, options?: Options) {
   const triggerElement = document.activeElement;
 
-  const focusableElement = container.querySelector<HTMLElement>(selectors);
+  const withDefaults = (options?: Options) => {
+    return {
+      active: options?.active ?? true,
+    };
+  };
 
-  // Use tick() to ensure focus trap works correctly inside <Portal />
-  void tick().then(() => focusableElement?.focus());
+  const setInitialFocus = () => {
+    const focusableElement = container.querySelector<HTMLElement>(selectors);
+    // Use tick() to ensure focus trap works correctly inside <Portal />
+    void tick().then(() => focusableElement?.focus());
+  };
+
+  if (withDefaults(options).active) {
+    setInitialFocus();
+  }
 
   const getFocusableElements = (): [HTMLElement | null, HTMLElement | null] => {
     const focusableElements = container.querySelectorAll<HTMLElement>(selectors);
@@ -27,7 +45,7 @@ export function focusTrap(container: HTMLElement) {
       shortcut: { key: 'Tab' },
       onShortcut: (event) => {
         const [firstElement, lastElement] = getFocusableElements();
-        if (document.activeElement === lastElement) {
+        if (document.activeElement === lastElement && withDefaults(options).active) {
           event.preventDefault();
           firstElement?.focus();
         }
@@ -39,7 +57,7 @@ export function focusTrap(container: HTMLElement) {
       shortcut: { key: 'Tab', shift: true },
       onShortcut: (event) => {
         const [firstElement, lastElement] = getFocusableElements();
-        if (document.activeElement === firstElement) {
+        if (document.activeElement === firstElement && withDefaults(options).active) {
           event.preventDefault();
           lastElement?.focus();
         }
@@ -48,6 +66,12 @@ export function focusTrap(container: HTMLElement) {
   ]);
 
   return {
+    update(newOptions?: Options) {
+      options = newOptions;
+      if (withDefaults(options).active) {
+        setInitialFocus();
+      }
+    },
     destroy() {
       destroyShortcuts?.();
       if (triggerElement instanceof HTMLElement) {
diff --git a/web/src/lib/components/elements/buttons/skip-link.svelte b/web/src/lib/components/elements/buttons/skip-link.svelte
index b80e7d1a44..a1a24634c4 100644
--- a/web/src/lib/components/elements/buttons/skip-link.svelte
+++ b/web/src/lib/components/elements/buttons/skip-link.svelte
@@ -7,10 +7,17 @@
      * Target for the skip link to move focus to.
      */
     target?: string;
+    /**
+     * Text for the skip link button.
+     */
     text?: string;
+    /**
+     * Breakpoint at which the skip link is visible. Defaults to always being visible.
+     */
+    breakpoint?: 'sm' | 'md' | 'lg' | 'xl' | '2xl';
   }
 
-  let { target = 'main', text = $t('skip_to_content') }: Props = $props();
+  let { target = 'main', text = $t('skip_to_content'), breakpoint }: Props = $props();
 
   let isFocused = $state(false);
 
@@ -18,6 +25,29 @@
     const targetEl = document.querySelector<HTMLElement>(target);
     targetEl?.focus();
   };
+
+  const getBreakpoint = () => {
+    if (!breakpoint) {
+      return '';
+    }
+    switch (breakpoint) {
+      case 'sm': {
+        return 'hidden sm:block';
+      }
+      case 'md': {
+        return 'hidden md:block';
+      }
+      case 'lg': {
+        return 'hidden lg:block';
+      }
+      case 'xl': {
+        return 'hidden xl:block';
+      }
+      case '2xl': {
+        return 'hidden 2xl:block';
+      }
+    }
+  };
 </script>
 
 <div class="absolute z-50 top-2 left-2 transition-transform {isFocused ? 'translate-y-0' : '-translate-y-10 sr-only'}">
@@ -25,6 +55,7 @@
     size="sm"
     rounded="none"
     onclick={moveFocus}
+    class={getBreakpoint()}
     onfocus={() => (isFocused = true)}
     onblur={() => (isFocused = false)}
   >
diff --git a/web/src/lib/components/layouts/user-page-layout.svelte b/web/src/lib/components/layouts/user-page-layout.svelte
index 27873f39a5..4944982b60 100644
--- a/web/src/lib/components/layouts/user-page-layout.svelte
+++ b/web/src/lib/components/layouts/user-page-layout.svelte
@@ -51,7 +51,7 @@
 </header>
 <main
   tabindex="-1"
-  class="relative grid h-screen grid-cols-[theme(spacing.18)_auto] overflow-hidden bg-immich-bg max-md:pt-[var(--navbar-height-md)] pt-[var(--navbar-height)] dark:bg-immich-dark-bg md:grid-cols-[theme(spacing.64)_auto]"
+  class="relative grid h-screen grid-cols-[theme(spacing.0)_auto] overflow-hidden bg-immich-bg max-md:pt-[var(--navbar-height-md)] pt-[var(--navbar-height)] dark:bg-immich-dark-bg md:grid-cols-[theme(spacing.64)_auto]"
 >
   {#if sidebar}{@render sidebar()}{:else if admin}
     <AdminSideBar />
@@ -66,7 +66,7 @@
       >
         <div class="flex gap-2 items-center">
           {#if title}
-            <div class="font-medium" tabindex="-1" id={headerId}>{title}</div>
+            <div class="font-medium outline-none" tabindex="-1" id={headerId}>{title}</div>
           {/if}
           {#if description}
             <p class="text-sm text-gray-400 dark:text-gray-600">{description}</p>
diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte
index d3afdc6072..7f716e70ef 100644
--- a/web/src/lib/components/photos-page/asset-grid.svelte
+++ b/web/src/lib/components/photos-page/asset-grid.svelte
@@ -726,7 +726,7 @@
   class={[
     'scrollbar-hidden h-full overflow-y-auto outline-none',
     { 'm-0': isEmpty },
-    { 'ml-4 tall:ml-0': !isEmpty },
+    { 'ml-0': !isEmpty },
     { 'mr-[60px]': !isEmpty && !usingMobileDevice },
   ]}
   tabindex="-1"
diff --git a/web/src/lib/components/photos-page/memory-lane.svelte b/web/src/lib/components/photos-page/memory-lane.svelte
index 9536aaf746..ff9264b961 100644
--- a/web/src/lib/components/photos-page/memory-lane.svelte
+++ b/web/src/lib/components/photos-page/memory-lane.svelte
@@ -38,7 +38,7 @@
   <section
     id="memory-lane"
     bind:this={memoryLaneElement}
-    class="relative mt-5 overflow-x-scroll overflow-y-hidden whitespace-nowrap transition-all"
+    class="relative mt-5 mx-2 overflow-x-scroll overflow-y-hidden whitespace-nowrap transition-all"
     style="scrollbar-width:none"
     use:resizeObserver={({ width }) => (offsetWidth = width)}
     onscroll={onScroll}
diff --git a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte
index 02b55a1d07..bd4bffd2f6 100644
--- a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte
+++ b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte
@@ -1,3 +1,7 @@
+<script lang="ts" module>
+  export const menuButtonId = 'top-menu-button';
+</script>
+
 <script lang="ts">
   import { page } from '$app/state';
   import { clickOutside } from '$lib/actions/click-outside';
@@ -12,13 +16,14 @@
   import { handleLogout } from '$lib/utils/auth';
   import { getAboutInfo, logout, type ServerAboutResponseDto } from '@immich/sdk';
   import { Button, IconButton } from '@immich/ui';
-  import { mdiHelpCircleOutline, mdiMagnify, mdiTrayArrowUp } from '@mdi/js';
+  import { mdiHelpCircleOutline, mdiMagnify, mdiMenu, mdiTrayArrowUp } from '@mdi/js';
   import { onMount } from 'svelte';
   import { t } from 'svelte-i18n';
   import { fade } from 'svelte/transition';
   import ThemeButton from '../theme-button.svelte';
   import UserAvatar from '../user-avatar.svelte';
   import AccountInfoPanel from './account-info-panel.svelte';
+  import { isSidebarOpen } from '$lib/stores/side-bar.svelte';
   import { mobileDevice } from '$lib/stores/mobile-device.svelte';
 
   interface Props {
@@ -57,11 +62,34 @@
 >
   <SkipLink text={$t('skip_to_content')} />
   <div
-    class="grid h-full grid-cols-[theme(spacing.18)_auto] items-center border-b bg-immich-bg py-2 dark:border-b-immich-dark-gray dark:bg-immich-dark-bg md:grid-cols-[theme(spacing.64)_auto]"
+    class="grid h-full grid-cols-[theme(spacing.32)_auto] items-center border-b bg-immich-bg py-2 dark:border-b-immich-dark-gray dark:bg-immich-dark-bg md:grid-cols-[theme(spacing.64)_auto]"
   >
-    <a data-sveltekit-preload-data="hover" class="ml-4" href={AppRoute.PHOTOS}>
-      <ImmichLogo class="max-md:h-[48px] h-[50px]" noText={mobileDevice.maxMd} />
-    </a>
+    <div class="flex flex-row gap-1 mx-4 items-center">
+      <div>
+        <IconButton
+          id={menuButtonId}
+          shape="round"
+          color="secondary"
+          variant="ghost"
+          size="large"
+          aria-label={$t('main_menu')}
+          icon={mdiMenu}
+          onclick={() => {
+            isSidebarOpen.value = !isSidebarOpen.value;
+          }}
+          onmousedown={(event: MouseEvent) => {
+            if (isSidebarOpen.value) {
+              // stops event from reaching the default handler when clicking outside of the sidebar
+              event.stopPropagation();
+            }
+          }}
+          class="md:hidden"
+        />
+      </div>
+      <a data-sveltekit-preload-data="hover" href={AppRoute.PHOTOS}>
+        <ImmichLogo class="max-md:h-[48px] h-[50px]" noText={mobileDevice.maxMd} />
+      </a>
+    </div>
     <div class="flex justify-between gap-4 lg:gap-8 pr-6">
       <div class="hidden w-full max-w-5xl flex-1 tall:pl-0 sm:block">
         {#if $featureFlags.search}
@@ -80,7 +108,6 @@
             href={AppRoute.SEARCH}
             id="search-button"
             class="sm:hidden"
-            title={$t('go_to_search')}
             aria-label={$t('go_to_search')}
           />
         {/if}
@@ -120,7 +147,6 @@
             color="secondary"
             variant="ghost"
             size="medium"
-            title={$t('support_and_feedback')}
             icon={mdiHelpCircleOutline}
             onclick={() => (shouldShowHelpPanel = !shouldShowHelpPanel)}
             aria-label={$t('support_and_feedback')}
diff --git a/web/src/lib/components/shared-components/purchasing/individual-purchase-option-card.svelte b/web/src/lib/components/shared-components/purchasing/individual-purchase-option-card.svelte
index c77a9dac96..815db8de9f 100644
--- a/web/src/lib/components/shared-components/purchasing/individual-purchase-option-card.svelte
+++ b/web/src/lib/components/shared-components/purchasing/individual-purchase-option-card.svelte
@@ -8,7 +8,9 @@
 </script>
 
 <!-- Individual Purchase Option -->
-<div class="border border-gray-300 dark:border-gray-800 w-[375px] p-8 rounded-3xl bg-gray-100 dark:bg-gray-900">
+<div
+  class="border border-gray-300 dark:border-gray-800 w-[min(375px,100%)] p-8 rounded-3xl bg-gray-100 dark:bg-gray-900"
+>
   <div class="text-immich-primary dark:text-immich-dark-primary">
     <Icon path={mdiAccount} size="56" />
     <p class="font-semibold text-lg mt-1">{$t('purchase_individual_title')}</p>
diff --git a/web/src/lib/components/shared-components/purchasing/purchase-content.svelte b/web/src/lib/components/shared-components/purchasing/purchase-content.svelte
index 6a4e7f1a4b..567fce9281 100644
--- a/web/src/lib/components/shared-components/purchasing/purchase-content.svelte
+++ b/web/src/lib/components/shared-components/purchasing/purchase-content.svelte
@@ -57,7 +57,7 @@
       </div>
     {/if}
 
-    <div class="flex gap-6 mt-4 justify-between">
+    <div class="flex flex-col sm:flex-row gap-6 mt-4 justify-between">
       <ServerPurchaseOptionCard />
       <UserPurchaseOptionCard />
     </div>
diff --git a/web/src/lib/components/shared-components/purchasing/server-purchase-option-card.svelte b/web/src/lib/components/shared-components/purchasing/server-purchase-option-card.svelte
index ffc015233c..19db461229 100644
--- a/web/src/lib/components/shared-components/purchasing/server-purchase-option-card.svelte
+++ b/web/src/lib/components/shared-components/purchasing/server-purchase-option-card.svelte
@@ -8,7 +8,9 @@
 </script>
 
 <!-- SERVER Purchase Options -->
-<div class="border border-gray-300 dark:border-gray-800 w-[375px] p-8 rounded-3xl bg-gray-100 dark:bg-gray-900">
+<div
+  class="border border-gray-300 dark:border-gray-800 w-[min(375px,100%)] p-8 rounded-3xl bg-gray-100 dark:bg-gray-900"
+>
   <div class="text-immich-primary dark:text-immich-dark-primary">
     <Icon path={mdiServer} size="56" />
     <p class="font-semibold text-lg mt-1">{$t('purchase_server_title')}</p>
diff --git a/web/src/lib/components/shared-components/side-bar/purchase-info.svelte b/web/src/lib/components/shared-components/side-bar/purchase-info.svelte
index a42e340eae..47e46c59b5 100644
--- a/web/src/lib/components/shared-components/side-bar/purchase-info.svelte
+++ b/web/src/lib/components/shared-components/side-bar/purchase-info.svelte
@@ -78,7 +78,7 @@
   <LicenseModal onClose={() => (isOpen = false)} />
 {/if}
 
-<div class="hidden md:block license-status pl-4 text-sm">
+<div class="license-status pl-4 text-sm">
   {#if $isPurchased && $preferences.purchase.showSupportBadge}
     <button
       onclick={() => goto(`${AppRoute.USER_SETTINGS}?isOpen=user-purchase-settings`)}
@@ -95,7 +95,7 @@
       onmouseleave={() => (hoverButton = false)}
       onfocus={onButtonHover}
       onblur={() => (hoverButton = false)}
-      class="p-2 flex justify-between place-items-center place-content-center border border-immich-primary/20 dark:border-immich-dark-primary/10 mt-2 rounded-lg shadow-md dark:bg-immich-dark-primary/10 w-full"
+      class="p-2 flex justify-between place-items-center place-content-center border border-immich-primary/20 dark:border-immich-dark-primary/10 mt-2 rounded-lg shadow-md dark:bg-immich-dark-primary/10 min-w-52 w-full"
     >
       <div class="flex justify-between w-full place-items-center place-content-center">
         <div class="flex place-items-center place-content-center gap-1">
@@ -110,7 +110,7 @@
         <div>
           <Icon
             path={mdiInformationOutline}
-            class="flex text-immich-primary dark:text-immich-dark-primary font-medium"
+            class="hidden md:flex text-immich-primary dark:text-immich-dark-primary font-medium"
             size="18"
           />
         </div>
@@ -123,7 +123,7 @@
   {#if showMessage}
     <dialog
       open
-      class="w-[500px] absolute bottom-[75px] left-[255px] bg-gray-50 dark:border-gray-800 border border-gray-200 dark:bg-immich-dark-gray dark:text-white text-black rounded-3xl z-10 shadow-2xl px-8 py-6"
+      class="hidden md:block w-[500px] absolute bottom-[75px] left-[255px] bg-gray-50 dark:border-gray-800 border border-gray-200 dark:bg-immich-dark-gray dark:text-white text-black rounded-3xl z-10 shadow-2xl px-8 py-6"
       transition:fade={{ duration: 150 }}
       onmouseover={() => (hoverMessage = true)}
       onmouseleave={() => (hoverMessage = false)}
diff --git a/web/src/lib/components/shared-components/side-bar/server-status.svelte b/web/src/lib/components/shared-components/side-bar/server-status.svelte
index e1d7340c46..8ca552a1f4 100644
--- a/web/src/lib/components/shared-components/side-bar/server-status.svelte
+++ b/web/src/lib/components/shared-components/side-bar/server-status.svelte
@@ -42,7 +42,7 @@
 {/if}
 
 <div
-  class="text-sm hidden group-hover:sm:flex md:flex pl-5 pr-1 place-items-center place-content-center justify-between"
+  class="text-sm flex md:flex pl-5 pr-1 place-items-center place-content-center justify-between min-w-52 overflow-hidden"
 >
   {#if $connected}
     <div class="flex gap-2 place-items-center place-content-center">
diff --git a/web/src/lib/components/shared-components/side-bar/side-bar-link.svelte b/web/src/lib/components/shared-components/side-bar/side-bar-link.svelte
index 47de0d5db0..f8bf89cd29 100644
--- a/web/src/lib/components/shared-components/side-bar/side-bar-link.svelte
+++ b/web/src/lib/components/shared-components/side-bar/side-bar-link.svelte
@@ -62,11 +62,9 @@
     class="flex w-full place-items-center gap-4 rounded-r-full py-3 transition-[padding] delay-100 duration-100 hover:cursor-pointer hover:bg-immich-gray hover:text-immich-primary dark:text-immich-dark-fg dark:hover:bg-immich-dark-gray dark:hover:text-immich-dark-primary
     {isSelected
       ? 'bg-immich-primary/10 text-immich-primary hover:bg-immich-primary/10 dark:bg-immich-dark-primary/10 dark:text-immich-dark-primary'
-      : ''}
-		pl-5 group-hover:sm:px-5 md:px-5
-  "
+      : ''}"
   >
-    <div class="flex w-full place-items-center gap-4 overflow-hidden truncate">
+    <div class="flex w-full place-items-center gap-4 pl-5 overflow-hidden truncate">
       <Icon path={icon} size="1.5em" class="shrink-0" flipped={flippedLogo} ariaHidden />
       <span class="text-sm font-medium">{title}</span>
     </div>
diff --git a/web/src/lib/components/shared-components/side-bar/side-bar-section.svelte b/web/src/lib/components/shared-components/side-bar/side-bar-section.svelte
index 97dbdc7637..4b22a16c9c 100644
--- a/web/src/lib/components/shared-components/side-bar/side-bar-section.svelte
+++ b/web/src/lib/components/shared-components/side-bar/side-bar-section.svelte
@@ -1,17 +1,56 @@
 <script lang="ts">
-  import type { Snippet } from 'svelte';
+  import { clickOutside } from '$lib/actions/click-outside';
+  import { focusTrap } from '$lib/actions/focus-trap';
+  import { menuButtonId } from '$lib/components/shared-components/navigation-bar/navigation-bar.svelte';
+  import { isSidebarOpen } from '$lib/stores/side-bar.svelte';
+  import { type Snippet } from 'svelte';
 
   interface Props {
     children?: Snippet;
   }
 
+  const mdBreakpoint = 768;
+
   let { children }: Props = $props();
+
+  let innerWidth: number = $state(0);
+
+  const closeSidebar = (width: number) => {
+    isSidebarOpen.value = width >= mdBreakpoint;
+  };
+
+  $effect(() => {
+    closeSidebar(innerWidth);
+  });
+
+  const isHidden = $derived(!isSidebarOpen.value && innerWidth < mdBreakpoint);
+  const isExpanded = $derived(isSidebarOpen.value && innerWidth < mdBreakpoint);
+
+  const handleClickOutside = () => {
+    if (!isSidebarOpen.value) {
+      return;
+    }
+    closeSidebar(innerWidth);
+    if (isHidden) {
+      document.querySelector<HTMLButtonElement>(`#${menuButtonId}`)?.focus();
+    }
+  };
 </script>
 
+<svelte:window bind:innerWidth />
 <section
   id="sidebar"
   tabindex="-1"
-  class="immich-scrollbar group relative z-10 flex w-18 flex-col gap-1 overflow-y-auto bg-immich-bg pt-8 max-md:pt-16 transition-all duration-200 dark:bg-immich-dark-bg hover:sm:w-64 hover:sm:border-r hover:sm:pr-6 hover:sm:shadow-2xl hover:sm:dark:border-r-immich-dark-gray md:w-64 md:pr-6 hover:md:border-none hover:md:shadow-none"
+  class="immich-scrollbar relative z-10 w-0 md:w-[16rem] overflow-y-auto overflow-x-hidden bg-immich-bg pt-8 transition-all duration-200 dark:bg-immich-dark-bg"
+  class:shadow-2xl={isExpanded}
+  class:dark:border-r-immich-dark-gray={isExpanded}
+  class:border-r={isExpanded}
+  class:w-[min(100vw,16rem)]={isSidebarOpen.value}
+  inert={isHidden}
+  use:clickOutside={{ onOutclick: handleClickOutside, onEscape: handleClickOutside }}
+  use:focusTrap={{ active: isExpanded }}
 >
-  {@render children?.()}
+  <div class="pr-6 flex flex-col gap-1 h-max min-h-full">
+    {@render children?.()}
+  </div>
 </section>
diff --git a/web/src/lib/components/shared-components/side-bar/side-bar.svelte b/web/src/lib/components/shared-components/side-bar/side-bar.svelte
index 5493495fd3..ec9c2a06da 100644
--- a/web/src/lib/components/shared-components/side-bar/side-bar.svelte
+++ b/web/src/lib/components/shared-components/side-bar/side-bar.svelte
@@ -84,10 +84,7 @@
       bind:isSelected={isSharingSelected}
     ></SideBarLink>
 
-    <div class="text-xs transition-all duration-200 dark:text-immich-dark-fg">
-      <p class="hidden p-6 group-hover:sm:block md:block">{$t('library').toUpperCase()}</p>
-      <hr class="mx-4 mb-[31px] mt-8 block group-hover:sm:hidden md:hidden" />
-    </div>
+    <p class="text-xs p-6 dark:text-immich-dark-fg">{$t('library').toUpperCase()}</p>
 
     <SideBarLink
       title={$t('favorites')}
diff --git a/web/src/lib/components/shared-components/side-bar/storage-space.svelte b/web/src/lib/components/shared-components/side-bar/storage-space.svelte
index 9472397565..604522c4f0 100644
--- a/web/src/lib/components/shared-components/side-bar/storage-space.svelte
+++ b/web/src/lib/components/shared-components/side-bar/storage-space.svelte
@@ -46,7 +46,7 @@
 </script>
 
 <div
-  class="hidden md:block storage-status p-4 bg-gray-100 dark:bg-immich-dark-primary/10 ml-4 rounded-lg text-sm"
+  class="storage-status p-4 bg-gray-100 dark:bg-immich-dark-primary/10 ml-4 rounded-lg text-sm min-w-52"
   title={$t('storage_usage', {
     values: {
       used: getByteUnitString(usedBytes, $locale, 3),
@@ -54,26 +54,24 @@
     },
   })}
 >
-  <div class="hidden group-hover:sm:block md:block">
-    <p class="font-medium text-immich-dark-gray dark:text-white mb-2">{$t('storage')}</p>
+  <p class="font-medium text-immich-dark-gray dark:text-white mb-2">{$t('storage')}</p>
 
-    {#if userInteraction.serverInfo}
-      <p class="text-gray-500 dark:text-gray-300">
-        {$t('storage_usage', {
-          values: {
-            used: getByteUnitString(usedBytes, $locale),
-            available: getByteUnitString(availableBytes, $locale),
-          },
-        })}
-      </p>
+  {#if userInteraction.serverInfo}
+    <p class="text-gray-500 dark:text-gray-300">
+      {$t('storage_usage', {
+        values: {
+          used: getByteUnitString(usedBytes, $locale),
+          available: getByteUnitString(availableBytes, $locale),
+        },
+      })}
+    </p>
 
-      <div class="mt-4 h-[7px] w-full rounded-full bg-gray-200 dark:bg-gray-700">
-        <div class="h-[7px] rounded-full {usageClasses}" style="width: {usedPercentage}%"></div>
-      </div>
-    {:else}
-      <div class="mt-2">
-        <LoadingSpinner />
-      </div>
-    {/if}
-  </div>
+    <div class="mt-4 h-[7px] w-full rounded-full bg-gray-200 dark:bg-gray-700">
+      <div class="h-[7px] rounded-full {usageClasses}" style="width: {usedPercentage}%"></div>
+    </div>
+  {:else}
+    <div class="mt-2">
+      <LoadingSpinner />
+    </div>
+  {/if}
 </div>
diff --git a/web/src/lib/components/shared-components/tree/tree.svelte b/web/src/lib/components/shared-components/tree/tree.svelte
index 33f9d14a13..ded6f70690 100644
--- a/web/src/lib/components/shared-components/tree/tree.svelte
+++ b/web/src/lib/components/shared-components/tree/tree.svelte
@@ -32,6 +32,7 @@
   href={getLink(path)}
   title={value}
   class={`flex flex-grow place-items-center pl-2 py-1 text-sm rounded-lg hover:bg-slate-200 dark:hover:bg-slate-800 hover:font-semibold ${isTarget ? 'bg-slate-100 dark:bg-slate-700 font-semibold text-immich-primary dark:text-immich-dark-primary' : 'dark:text-gray-200'}`}
+  data-sveltekit-keepfocus
 >
   <button type="button" {onclick} class={Object.values(tree).length === 0 ? 'invisible' : ''}>
     <Icon path={isOpen ? mdiChevronDown : mdiChevronRight} class="text-gray-400" size={20} />
diff --git a/web/src/lib/stores/side-bar.svelte.ts b/web/src/lib/stores/side-bar.svelte.ts
new file mode 100644
index 0000000000..791ee32c91
--- /dev/null
+++ b/web/src/lib/stores/side-bar.svelte.ts
@@ -0,0 +1 @@
+export const isSidebarOpen = $state({ value: false });
diff --git a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte
index d0d7bc3359..f5a4f6cd09 100644
--- a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -131,7 +131,7 @@
 <UserPageLayout title={data.meta.title}>
   {#snippet sidebar()}
     <SideBarSection>
-      <SkipLink target={`#${headerId}`} text={$t('skip_to_folders')} />
+      <SkipLink target={`#${headerId}`} text={$t('skip_to_folders')} breakpoint="md" />
       <section>
         <div class="text-xs pl-4 mb-2 dark:text-white">{$t('explorer').toUpperCase()}</div>
         <div class="h-full">
diff --git a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte
index a89da7ad6b..bf423e4825 100644
--- a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -148,7 +148,7 @@
 <UserPageLayout title={data.meta.title}>
   {#snippet sidebar()}
     <SideBarSection>
-      <SkipLink target={`#${headerId}`} text={$t('skip_to_tags')} />
+      <SkipLink target={`#${headerId}`} text={$t('skip_to_tags')} breakpoint="md" />
       <section>
         <div class="text-xs pl-4 mb-2 dark:text-white">{$t('explorer').toUpperCase()}</div>
         <div class="h-full">