diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte
index 4fa7d72a2f..46c95636d0 100644
--- a/web/src/lib/components/asset-viewer/asset-viewer.svelte
+++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte
@@ -12,7 +12,7 @@
   import { stackAssetsStore } from '$lib/stores/stacked-asset.store';
   import { user } from '$lib/stores/user.store';
   import { getAssetJobMessage, getSharedLink, handlePromiseError, isSharedLink } from '$lib/utils';
-  import { addAssetsToAlbum, addAssetsToNewAlbum, downloadFile } from '$lib/utils/asset-utils';
+  import { addAssetsToAlbum, addAssetsToNewAlbum, downloadFile, unstackAssets } from '$lib/utils/asset-utils';
   import { handleError } from '$lib/utils/handle-error';
   import { shortcuts } from '$lib/utils/shortcut';
   import { SlideshowHistory } from '$lib/utils/slideshow-history';
@@ -28,7 +28,6 @@
     getAllAlbums,
     runAssetJobs,
     updateAsset,
-    updateAssets,
     updateAlbumInfo,
     type ActivityResponseDto,
     type AlbumResponseDto,
@@ -481,20 +480,15 @@
   };
 
   const handleUnstack = async () => {
-    try {
-      const ids = $stackAssetsStore.map(({ id }) => id);
-      await updateAssets({ assetBulkUpdateDto: { ids, removeParent: true } });
-      for (const child of $stackAssetsStore) {
-        child.stackParentId = null;
-        child.stackCount = 0;
-        child.stack = [];
-        dispatch('action', { type: AssetAction.ADD, asset: child });
+    const unstackedAssets = await unstackAssets($stackAssetsStore);
+    if (unstackedAssets) {
+      for (const asset of unstackedAssets) {
+        dispatch('action', {
+          type: AssetAction.ADD,
+          asset,
+        });
       }
-
       dispatch('close');
-      notificationController.show({ type: NotificationType.Info, message: 'Un-stacked', timeout: 1500 });
-    } catch (error) {
-      handleError(error, `Unable to unstack`);
     }
   };
 
diff --git a/web/src/lib/components/photos-page/actions/stack-action.svelte b/web/src/lib/components/photos-page/actions/stack-action.svelte
index a857da1dd3..b6a034672b 100644
--- a/web/src/lib/components/photos-page/actions/stack-action.svelte
+++ b/web/src/lib/components/photos-page/actions/stack-action.svelte
@@ -1,20 +1,45 @@
 <script lang="ts">
   import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
-  import { getAssetControlContext } from '../asset-select-control-bar.svelte';
-  import type { OnStack } from '$lib/utils/actions';
-  import { stackAssets } from '$lib/utils/asset-utils';
-  import { mdiImageMultipleOutline } from '@mdi/js';
+  import { getAssetControlContext } from '$lib/components/photos-page/asset-select-control-bar.svelte';
+  import { mdiImageMinusOutline, mdiImageMultipleOutline } from '@mdi/js';
+  import { stackAssets, unstackAssets } from '$lib/utils/asset-utils';
+  import type { OnStack, OnUnstack } from '$lib/utils/actions';
 
+  export let unstack = false;
   export let onStack: OnStack | undefined;
+  export let onUnstack: OnUnstack | undefined;
 
   const { clearSelect, getOwnedAssets } = getAssetControlContext();
 
   const handleStack = async () => {
-    await stackAssets([...getOwnedAssets()], (ids) => {
+    const selectedAssets = [...getOwnedAssets()];
+    const ids = await stackAssets(selectedAssets);
+    if (ids) {
       onStack?.(ids);
       clearSelect();
-    });
+    }
+  };
+
+  const handleUnstack = async () => {
+    const selectedAssets = [...getOwnedAssets()];
+    if (selectedAssets.length !== 1) {
+      return;
+    }
+    const { stack } = selectedAssets[0];
+    if (!stack) {
+      return;
+    }
+    const assets = [selectedAssets[0], ...stack];
+    const unstackedAssets = await unstackAssets(assets);
+    if (unstackedAssets) {
+      onUnstack?.(unstackedAssets);
+    }
+    clearSelect();
   };
 </script>
 
-<MenuOption text="Stack" icon={mdiImageMultipleOutline} on:click={handleStack} />
+{#if unstack}
+  <MenuOption text="Un-stack" icon={mdiImageMinusOutline} on:click={handleUnstack} />
+{:else}
+  <MenuOption text="Stack" icon={mdiImageMultipleOutline} on:click={handleStack} />
+{/if}
diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte
index 06b5627d1b..a84b9d4d73 100644
--- a/web/src/lib/components/photos-page/asset-grid.svelte
+++ b/web/src/lib/components/photos-page/asset-grid.svelte
@@ -89,11 +89,10 @@
   };
 
   const onStackAssets = async () => {
-    if ($selectedAssets.size > 1) {
-      await stackAssets(Array.from($selectedAssets), (ids) => {
-        assetStore.removeAssets(ids);
-        dispatch('escape');
-      });
+    const ids = await stackAssets(Array.from($selectedAssets));
+    if (ids) {
+      assetStore.removeAssets(ids);
+      dispatch('escape');
     }
   };
 
diff --git a/web/src/lib/utils/actions.ts b/web/src/lib/utils/actions.ts
index b6718e63a1..ecfd29a8fc 100644
--- a/web/src/lib/utils/actions.ts
+++ b/web/src/lib/utils/actions.ts
@@ -1,5 +1,5 @@
 import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification';
-import { deleteAssets as deleteBulk } from '@immich/sdk';
+import { deleteAssets as deleteBulk, type AssetResponseDto } from '@immich/sdk';
 import { handleError } from './handle-error';
 
 export type OnDelete = (assetIds: string[]) => void;
@@ -7,6 +7,7 @@ export type OnRestore = (ids: string[]) => void;
 export type OnArchive = (ids: string[], isArchived: boolean) => void;
 export type OnFavorite = (ids: string[], favorite: boolean) => void;
 export type OnStack = (ids: string[]) => void;
+export type OnUnstack = (assets: AssetResponseDto[]) => void;
 
 export const deleteAssets = async (force: boolean, onAssetDelete: OnDelete, ids: string[]) => {
   try {
diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts
index 72102d2634..50337fb06b 100644
--- a/web/src/lib/utils/asset-utils.ts
+++ b/web/src/lib/utils/asset-utils.ts
@@ -2,6 +2,7 @@ import { goto } from '$app/navigation';
 import { NotificationType, notificationController } from '$lib/components/shared-components/notification/notification';
 import { AppRoute } from '$lib/constants';
 import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store';
+import { assetViewingStore } from '$lib/stores/asset-viewing.store';
 import { BucketPosition, isSelectingAllAssets, type AssetStore } from '$lib/stores/assets.store';
 import { downloadManager } from '$lib/stores/download';
 import { downloadRequest, getKey } from '$lib/utils';
@@ -269,43 +270,81 @@ export const getSelectedAssets = (assets: Set<AssetResponseDto>, user: UserRespo
   return ids;
 };
 
-export async function stackAssets(assets: Array<AssetResponseDto>, onStack: (ds: string[]) => void) {
+export const stackAssets = async (assets: AssetResponseDto[]) => {
+  if (assets.length < 2) {
+    return false;
+  }
+
+  const parent = assets[0];
+  const children = assets.slice(1);
+  const ids = children.map(({ id }) => id);
+
   try {
-    const parent = assets.at(0);
-    if (!parent) {
-      return;
-    }
+    await updateAssets({
+      assetBulkUpdateDto: {
+        ids,
+        stackParentId: parent.id,
+      },
+    });
+  } catch (error) {
+    handleError(error, 'Failed to stack assets');
+    return false;
+  }
 
-    const children = assets.slice(1);
-    const ids = children.map(({ id }) => id);
-
-    if (children.length > 0) {
-      await updateAssets({ assetBulkUpdateDto: { ids, stackParentId: parent.id } });
-    }
-
-    let childrenCount = parent.stackCount || 1;
-    for (const asset of children) {
-      asset.stackParentId = parent.id;
-      // Add grand-children's count to new parent
-      childrenCount += asset.stackCount || 1;
+  let grandChildren: AssetResponseDto[] = [];
+  for (const asset of children) {
+    asset.stackParentId = parent.id;
+    if (asset.stack) {
+      // Add grand-children to new parent
+      grandChildren = grandChildren.concat(asset.stack);
       // Reset children stack info
       asset.stackCount = null;
       asset.stack = [];
     }
-
-    parent.stackCount = childrenCount;
-
-    notificationController.show({
-      message: `Stacked ${ids.length + 1} assets`,
-      type: NotificationType.Info,
-      timeout: 1500,
-    });
-
-    onStack(ids);
-  } catch (error) {
-    handleError(error, `Unable to stack`);
   }
-}
+
+  parent.stack ??= [];
+  parent.stack = parent.stack.concat(children, grandChildren);
+  parent.stackCount = parent.stack.length + 1;
+
+  notificationController.show({
+    message: `Stacked ${parent.stackCount} assets`,
+    type: NotificationType.Info,
+    button: {
+      text: 'View Stack',
+      onClick() {
+        return assetViewingStore.setAssetId(parent.id);
+      },
+    },
+  });
+
+  return ids;
+};
+
+export const unstackAssets = async (assets: AssetResponseDto[]) => {
+  const ids = assets.map(({ id }) => id);
+  try {
+    await updateAssets({
+      assetBulkUpdateDto: {
+        ids,
+        removeParent: true,
+      },
+    });
+  } catch (error) {
+    handleError(error, 'Failed to un-stack assets');
+    return;
+  }
+  for (const asset of assets) {
+    asset.stackParentId = null;
+    asset.stackCount = null;
+    asset.stack = [];
+  }
+  notificationController.show({
+    type: NotificationType.Info,
+    message: `Un-stacked ${assets.length} assets`,
+  });
+  return assets;
+};
 
 export const selectAllAssets = async (assetStore: AssetStore, assetInteractionStore: AssetInteractionStore) => {
   if (get(isSelectingAllAssets)) {
diff --git a/web/src/routes/(user)/photos/+page.svelte b/web/src/routes/(user)/photos/+page.svelte
index 00409cdc1b..f711b081d6 100644
--- a/web/src/routes/(user)/photos/+page.svelte
+++ b/web/src/routes/(user)/photos/+page.svelte
@@ -30,7 +30,14 @@
   const assetInteractionStore = createAssetInteractionStore();
   const { isMultiSelectState, selectedAssets } = assetInteractionStore;
 
-  $: isAllFavorite = [...$selectedAssets].every((asset) => asset.isFavorite);
+  let isAllFavorite: boolean;
+  let isAssetStackSelected: boolean;
+
+  $: {
+    const selection = [...$selectedAssets];
+    isAllFavorite = selection.every((asset) => asset.isFavorite);
+    isAssetStackSelected = selection.length === 1 && !!selection[0].stack;
+  }
 
   const handleEscape = () => {
     if ($showAssetViewer) {
@@ -62,8 +69,12 @@
     <FavoriteAction removeFavorite={isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} />
     <AssetSelectContextMenu icon={mdiDotsVertical} title="Menu">
       <DownloadAction menuItem />
-      {#if $selectedAssets.size > 1}
-        <StackAction onStack={(assetIds) => assetStore.removeAssets(assetIds)} />
+      {#if $selectedAssets.size > 1 || isAssetStackSelected}
+        <StackAction
+          unstack={isAssetStackSelected}
+          onStack={(assetIds) => assetStore.removeAssets(assetIds)}
+          onUnstack={(assets) => assetStore.addAssets(assets)}
+        />
       {/if}
       <ChangeDate menuItem />
       <ChangeLocation menuItem />