diff --git a/frontend/public/images/icons/favicon-tracking-32x32.png b/frontend/public/images/icons/favicon-tracking-32x32.png
new file mode 100644
index 000000000..d3867376f
Binary files /dev/null and b/frontend/public/images/icons/favicon-tracking-32x32.png differ
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index 08688b1bb..760c18edc 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -61,6 +61,7 @@ import {useAuthStore} from '@/stores/auth'
import {useBaseStore} from '@/stores/base'
import {useColorScheme} from '@/composables/useColorScheme'
+import {useTimeTrackingFavicon} from '@/composables/useTimeTrackingFavicon'
import {useBodyClass} from '@/composables/useBodyClass'
import QuickAddOverlay from '@/components/quick-actions/QuickAddOverlay.vue'
import AddToHomeScreen from '@/components/home/AddToHomeScreen.vue'
@@ -107,6 +108,7 @@ watch(accountDeletionConfirm, async (accountDeletionConfirm) => {
setLanguage(authStore.settings.language ?? DEFAULT_LANGUAGE)
useColorScheme()
+useTimeTrackingFavicon()
diff --git a/frontend/src/composables/useTimeTrackingFavicon.ts b/frontend/src/composables/useTimeTrackingFavicon.ts
new file mode 100644
index 000000000..43f24b8a7
--- /dev/null
+++ b/frontend/src/composables/useTimeTrackingFavicon.ts
@@ -0,0 +1,32 @@
+import {watch} from 'vue'
+import {createSharedComposable, tryOnMounted} from '@vueuse/core'
+import {storeToRefs} from 'pinia'
+
+import {useTimeTrackingStore} from '@/stores/timeTracking'
+import {getFullBaseUrl} from '@/helpers/getFullBaseUrl'
+
+const TRACKING_FAVICON = `${getFullBaseUrl()}images/icons/favicon-tracking-32x32.png`
+
+function getFaviconLink(): HTMLLinkElement | null {
+ return document.querySelector('link[rel="icon"]')
+}
+
+// Swaps in a favicon with a small red dot in the lower left corner while a timer
+// is running, so an active time tracking session is visible even when the tab
+// isn't focused.
+export const useTimeTrackingFavicon = createSharedComposable(() => {
+ const {hasActiveTimer} = storeToRefs(useTimeTrackingStore())
+
+ const originalHref = getFaviconLink()?.getAttribute('href') ?? '/favicon.ico'
+
+ function update(active: boolean) {
+ const link = getFaviconLink()
+ if (link === null) {
+ return
+ }
+ link.href = active ? TRACKING_FAVICON : originalHref
+ }
+
+ watch(hasActiveTimer, update, {flush: 'post'})
+ tryOnMounted(() => update(hasActiveTimer.value))
+})