diff --git a/web/i18n/en/a11y/dialog.json b/web/i18n/en/a11y/dialog.json
index 84df736b..478a1587 100644
--- a/web/i18n/en/a11y/dialog.json
+++ b/web/i18n/en/a11y/dialog.json
@@ -1,5 +1,7 @@
 {
     "picker.item.photo": "photo thumbnail",
     "picker.item.video": "video thumbnail",
-    "picker.item.gif": "gif thumbnail"
+    "picker.item.gif": "gif thumbnail",
+
+    "saving.copied": "link copied"
 }
diff --git a/web/src/components/buttons/VerticalActionButton.svelte b/web/src/components/buttons/VerticalActionButton.svelte
index a9d37689..14602500 100644
--- a/web/src/components/buttons/VerticalActionButton.svelte
+++ b/web/src/components/buttons/VerticalActionButton.svelte
@@ -5,9 +5,17 @@
     };
     export let fill = false;
     export let elevated = false;
+    export let ariaLabel = "";
 </script>
 
-<button id="button-{id}" class="button vertical" class:fill class:elevated on:click={click}>
+<button
+    id="button-{id}"
+    class="button vertical"
+    class:fill
+    class:elevated
+    on:click={click}
+    aria-label={ariaLabel}
+>
     <slot></slot>
 </button>
 
@@ -19,6 +27,11 @@
         gap: calc(var(--padding) / 2);
     }
 
+    .button.vertical :global(svg) {
+        width: 24px;
+        height: 24px;
+    }
+
     .button.vertical.fill {
         width: 100%;
         padding: var(--padding) 0;
diff --git a/web/src/components/dialog/SavingDialog.svelte b/web/src/components/dialog/SavingDialog.svelte
index bba591f1..ee0a5ee4 100644
--- a/web/src/components/dialog/SavingDialog.svelte
+++ b/web/src/components/dialog/SavingDialog.svelte
@@ -10,16 +10,25 @@
     import DialogButtons from "$components/dialog/DialogButtons.svelte";
     import VerticalActionButton from "$components/buttons/VerticalActionButton.svelte";
 
-    import IconCopy from "@tabler/icons-svelte/IconCopy.svelte";
     import IconShare2 from "@tabler/icons-svelte/IconShare2.svelte";
     import IconDownload from "@tabler/icons-svelte/IconDownload.svelte";
     import IconFileDownload from "@tabler/icons-svelte/IconFileDownload.svelte";
 
+    import CopyIcon from "$components/misc/CopyIcon.svelte";
+
     export let id: string;
     export let url: string;
     export let bodyText: string = "";
 
     let close: () => void;
+
+    let copied = false;
+
+    $: if (copied) {
+        setTimeout(() => {
+            copied = false;
+        }, 1500);
+    }
 </script>
 
 <DialogContainer {id} bind:close>
@@ -63,18 +72,24 @@
                     id="save-copy"
                     fill
                     elevated
-                    click={async () => copyURL(url)}
+                    click={async () => {
+                        copyURL(url);
+                        copied = true;
+                    }}
+                    ariaLabel={copied ? $t("a11y.dialog.saving.copied") : ""}
                 >
-                    <IconCopy />
+                    <CopyIcon check={copied} />
                     {$t("dialog.button.copy")}
                 </VerticalActionButton>
             </div>
         </div>
+
         {#if bodyText}
             <div class="body-text">
                 {bodyText}
             </div>
         {/if}
+
         <DialogButtons
             buttons={[
                 {
diff --git a/web/src/components/misc/CopyIcon.svelte b/web/src/components/misc/CopyIcon.svelte
new file mode 100644
index 00000000..2bb60b01
--- /dev/null
+++ b/web/src/components/misc/CopyIcon.svelte
@@ -0,0 +1,54 @@
+<script lang="ts">
+    import IconCopy from "@tabler/icons-svelte/IconCopy.svelte";
+    import IconCheck from "@tabler/icons-svelte/IconCheck.svelte";
+
+    export let check = false;
+</script>
+
+<div id="copy-animation" class:check>
+    <div class="icon-copy">
+        <IconCopy />
+    </div>
+    <div class="icon-check">
+        <IconCheck />
+    </div>
+</div>
+
+<style>
+    #copy-animation {
+        position: relative;
+        height: 24px;
+        width: 24px;
+    }
+
+    #copy-animation :global(svg) {
+        will-change: transform;
+    }
+
+    .icon-copy,
+    .icon-check {
+        display: flex;
+        position: absolute;
+        transition: transform 0.25s, opacity 0.25s;
+    }
+
+    .icon-copy {
+        transform: none;
+        opacity: 1;
+    }
+
+    .icon-check {
+        transform: scale(0.4);
+        opacity: 0;
+    }
+
+    .check .icon-copy {
+        transform: scale(0.4);
+        opacity: 0;
+    }
+
+    .check .icon-check {
+        transform: none;
+        opacity: 1;
+    }
+</style>