diff --git a/src/lib/components/Spinner.svelte b/src/lib/components/Spinner.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..0732f2b7dd37f53a3bb0cc2a315e5d1bfb304e83
--- /dev/null
+++ b/src/lib/components/Spinner.svelte
@@ -0,0 +1,64 @@
+<script lang="ts">
+  // Code inspired from: https://github.com/Schum123/svelte-loading-spinners/blob/master/src/Circle2.svelte
+  // MIT license.
+
+  export let size: string | number = "60"
+  export let unit: string = "px"
+  export let colorOuter: string = "#FF3E00"
+  export let colorCenter: string = "#40B3FF"
+  export let colorInner: string = "#676778"
+  export let durationMultiplier: number = 1
+  export let durationOuter: string = `${durationMultiplier * 2}s`
+  export let durationInner: string = `${durationMultiplier * 1.5}s`
+  export let durationCenter: string = `${durationMultiplier * 3}s`
+</script>
+
+<div
+  class="circle mx-auto my-8"
+  style="--size: {size}{unit}; --colorInner: {colorInner}; --colorCenter: {colorCenter}; --colorOuter: {colorOuter}; --durationInner: {durationInner}; --durationCenter: {durationCenter}; --durationOuter: {durationOuter};"
+/>
+
+<style>
+  .circle {
+    width: var(--size);
+    height: var(--size);
+    box-sizing: border-box;
+    position: relative;
+    border: 3px solid transparent;
+    border-top-color: var(--colorOuter);
+    border-radius: 50%;
+    animation: circleSpin var(--durationOuter) linear infinite;
+  }
+  .circle:before,
+  .circle:after {
+    content: "";
+    box-sizing: border-box;
+    position: absolute;
+    border: 3px solid transparent;
+    border-radius: 50%;
+  }
+  .circle:after {
+    border-top-color: var(--colorInner);
+    top: 9px;
+    left: 9px;
+    right: 9px;
+    bottom: 9px;
+    animation: circleSpin var(--durationInner) linear infinite;
+  }
+  .circle:before {
+    border-top-color: var(--colorCenter);
+    top: 3px;
+    left: 3px;
+    right: 3px;
+    bottom: 3px;
+    animation: circleSpin var(--durationCenter) linear infinite;
+  }
+  @keyframes circleSpin {
+    0% {
+      transform: rotate(0deg);
+    }
+    100% {
+      transform: rotate(360deg);
+    }
+  }
+</style>
diff --git a/src/lib/components/WaterfallView.svelte b/src/lib/components/WaterfallView.svelte
index a3f4fd32d609e36893c2055783b933221718cf1e..c9bd80cf14d2d8ea009f13b1ccf6e062d1d062f1 100644
--- a/src/lib/components/WaterfallView.svelte
+++ b/src/lib/components/WaterfallView.svelte
@@ -4,8 +4,9 @@
   import type { Writable } from "svelte/store"
 
   import { goto } from "$app/navigation"
-  import { session } from "$app/stores"
+  import Spinner from "$lib/components/Spinner.svelte"
   import type {
+    CalculationName,
     DecompositionByName,
     EvaluationByName,
     VisibleDecomposition,
@@ -27,6 +28,9 @@
   export let year: number
 
   const adaptAmountsScale = getContext("adaptAmountsScale") as Writable<boolean>
+  const calculationRunningByName = getContext(
+    "calculationRunningByName",
+  ) as Writable<{ [name in CalculationName]?: boolean }>
   const dateFormatter = new Intl.DateTimeFormat("fr-FR", { dateStyle: "full" })
   const deltaFormatter = new Intl.NumberFormat("fr-FR", {
     currency: "EUR",
@@ -145,7 +149,12 @@
 </script>
 
 {#if visibleDecompositions.length > 0}
-  <div class="flex pr-2 mt-4">
+  <div class="flex mt-4 pr-2 relative">
+    {#if Object.keys($calculationRunningByName).length > 0}
+      <div class="absolute inset-0 bg-gray-300 bg-opacity-50 z-50">
+        <Spinner />
+      </div>
+    {/if}
     <!-- partie gauche dispositifs et ticket de caisse-->
     <div class="bg-opacity-40 shadow-lg w-4/5">
       <div class="flex justify-between">
diff --git a/src/lib/components/test_cases/TestCaseView.svelte b/src/lib/components/test_cases/TestCaseView.svelte
index 3060bb9a301158fe8ddf33eca9b8545d1a25b2ce..c9f104025d29c41965b684b8c773c58f4ad56cd7 100644
--- a/src/lib/components/test_cases/TestCaseView.svelte
+++ b/src/lib/components/test_cases/TestCaseView.svelte
@@ -7,6 +7,7 @@
   import PictoArbreMetropole from "$lib/components/pictos/PictoArbreMetropole.svelte"
   import PictoFemme from "$lib/components/pictos/PictoFemme.svelte"
   import PictoEnfant from "$lib/components/pictos/PictoEnfant.svelte"
+  import Spinner from "$lib/components/Spinner.svelte"
   import ValueChange from "$lib/components/test_cases/ValueChange.svelte"
   import WaterfallView from "$lib/components/WaterfallView.svelte"
   import type {
@@ -41,6 +42,9 @@
   const advanced = $session.advanced
   let axisDescription: AxisDescription | null = null
   const billName = getContext("billName") as Writable<string | undefined>
+  const calculationRunningByName = getContext(
+    "calculationRunningByName",
+  ) as Writable<{ [name in CalculationName]?: boolean }>
   const childrenKey = $session.childrenKey
   const dispatch = createEventDispatcher()
   const enfantVariablesName = ["age"]
@@ -411,8 +415,13 @@
     {/if}
   </div>
   <div
-    class="px-4 py-2 bg-gray-100 grid grid-flow-col grid-cols-{waterfalls.length} grid-rows-3 gap-x-4 "
+    class="px-4 py-2 bg-gray-100 grid grid-flow-col grid-cols-{waterfalls.length} grid-rows-3 gap-x-4 relative"
   >
+    {#if Object.keys($calculationRunningByName).length > 0}
+      <div class="absolute inset-0 bg-gray-300 bg-opacity-50 z-50">
+        <Spinner />
+      </div>
+    {/if}
     {#each waterfalls as { icon, label, root, total, totalLabel }}
       <div class="place-self-center">
         {#if icon !== undefined}
diff --git a/src/routes/__layout.svelte b/src/routes/__layout.svelte
index 05cbfe6e92b5944c24f45f7a7a81b13af90ac426..1a680a773a4e242ef35150d6b222acf0675fdf6c 100644
--- a/src/routes/__layout.svelte
+++ b/src/routes/__layout.svelte
@@ -49,6 +49,11 @@
   const calculationName: Writable<CalculationName> = writable("law")
   setContext("calculationName", calculationName)
 
+  const calculationRunningByName: Writable<{
+    [name in CalculationName]?: boolean
+  }> = writable({})
+  setContext("calculationRunningByName", calculationRunningByName)
+
   const calculationTokenByName: Writable<{
     [name in CalculationName]?: string
   }> = writable({})
@@ -340,6 +345,9 @@
                   console.log(
                     `Ignoring API ${calculationName} response with invalid token: ${result.token} instead of ${$calculationTokenByName[calculationName]}`,
                   )
+                } else if (result.done) {
+                  $calculationRunningByName = { ...$calculationRunningByName }
+                  delete $calculationRunningByName[calculationName]
                 } else {
                   // Count total population of test cases.
                   const entity = entityByKey[result.entity]
diff --git a/src/routes/index.svelte b/src/routes/index.svelte
index 3d9d1e9d0b7aa179e775b184ba66a8646f397366..481aa3d0ba20aa74b5ecb261ef4d25680c75b073 100644
--- a/src/routes/index.svelte
+++ b/src/routes/index.svelte
@@ -68,6 +68,9 @@
   const calculationName = getContext(
     "calculationName",
   ) as Writable<CalculationName>
+  const calculationRunningByName = getContext(
+    "calculationRunningByName",
+  ) as Writable<{ [name in CalculationName]?: boolean }>
   const calculationTokenByName = getContext(
     "calculationTokenByName",
   ) as Writable<{ [name in CalculationName]?: string }>
@@ -556,6 +559,10 @@
     }
     if (requestedCalculationsName.has("law") && $webSocketOpenByName.law) {
       const token = ($calculationTokenByName.law = uuidv4())
+      $calculationRunningByName = {
+        ...$calculationRunningByName,
+        law: true,
+      }
       $webSocketByName.law.send(
         JSON.stringify({ ...message, title: "law", token }),
       )
@@ -566,6 +573,10 @@
       $webSocketOpenByName.bill
     ) {
       const token = ($calculationTokenByName.bill = uuidv4())
+      $calculationRunningByName = {
+        ...$calculationRunningByName,
+        bill: true,
+      }
       $webSocketByName.bill.send(
         JSON.stringify({
           ...message,
@@ -574,6 +585,11 @@
           token,
         }),
       )
+    } else if ($calculationRunningByName.bill) {
+      $calculationRunningByName = {
+        ...$calculationRunningByName,
+      }
+      delete $calculationRunningByName.bill
     }
     if (
       requestedCalculationsName.has("amendment") &&
@@ -581,6 +597,10 @@
       $webSocketOpenByName.amendment
     ) {
       const token = ($calculationTokenByName.amendment = uuidv4())
+      $calculationRunningByName = {
+        ...$calculationRunningByName,
+        amendment: true,
+      }
       $webSocketByName.amendment.send(
         JSON.stringify({
           ...message,
@@ -590,6 +610,11 @@
           token,
         }),
       )
+    } else if ($calculationRunningByName.amendment) {
+      $calculationRunningByName = {
+        ...$calculationRunningByName,
+      }
+      delete $calculationRunningByName.amendment
     }
   }