diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index e54155ec..495ec7b4 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -1530,6 +1530,10 @@
     "message": "The value cannot be saved. Try reducing the amount of text.",
     "description": "Displayed when trying to save an excessively big value via storage.sync API"
   },
+  "syncErrorRelogin": {
+    "message": "Sync failed.\nPlease re-login.",
+    "description": "Tooltip for the toolbar icon"
+  },
   "toggleStyle": {
     "message": "Toggle style",
     "description": "Label for the checkbox to enable/disable a style"
diff --git a/background/icon-manager.js b/background/icon-manager.js
index 5d51e8e7..122b7e5b 100644
--- a/background/icon-manager.js
+++ b/background/icon-manager.js
@@ -5,10 +5,12 @@
 /* global CHROME FIREFOX VIVALDI debounce ignoreChromeError */// toolbox.js
 'use strict';
 
-(() => {
+/* exported iconMan */
+const iconMan = (() => {
   const ICON_SIZES = FIREFOX || CHROME >= 55 && !VIVALDI ? [16, 32] : [19, 38];
   const staleBadges = new Set();
   const imageDataCache = new Map();
+  const badgeOvr = {color: '', text: ''};
   // https://github.com/openstyles/stylus/issues/335
   let hasCanvas = loadImage(`/images/icon/${ICON_SIZES[0]}.png`)
     .then(({data}) => (hasCanvas = data.some(b => b !== 255)));
@@ -54,6 +56,29 @@
     ], () => debounce(refreshAllIcons), {runNow: true});
   });
 
+  return {
+    /** Calling with no params clears the override */
+    overrideBadge({text = '', color = ''} = {}) {
+      if (badgeOvr.text === text && badgeOvr.color === color) {
+        return;
+      }
+      badgeOvr.text = text;
+      badgeOvr.color = color;
+      refreshIconBadgeColor();
+      setBadgeText({text});
+      for (const tabId of tabMan.list()) {
+        if (badgeOvr) {
+          setBadgeText({tabId, text});
+        } else {
+          refreshIconBadgeText(tabId);
+        }
+      }
+      chrome.browserAction.setTitle({
+        title: text ? chrome.i18n.getMessage('syncErrorRelogin') : '',
+      }, ignoreChromeError);
+    },
+  };
+
   function onPortDisconnected({sender}) {
     if (tabMan.get(sender.tab.id, 'styleIds')) {
       API.updateIconBadge.call({sender}, [], {lazyBadge: true});
@@ -61,6 +86,7 @@
   }
 
   function refreshIconBadgeText(tabId) {
+    if (badgeOvr.text) return;
     const text = prefs.get('show-badge') ? `${getStyleCount(tabId)}` : '';
     setBadgeText({tabId, text});
   }
@@ -133,9 +159,9 @@
   }
 
   function refreshIconBadgeColor() {
-    const color = prefs.get(prefs.get('disableAll') ? 'badgeDisabled' : 'badgeNormal');
     setBadgeBackgroundColor({
-      color,
+      color: badgeOvr.color ||
+        prefs.get(prefs.get('disableAll') ? 'badgeDisabled' : 'badgeNormal'),
     });
   }
 
diff --git a/background/sync-manager.js b/background/sync-manager.js
index 0d5938f5..48f3c7a3 100644
--- a/background/sync-manager.js
+++ b/background/sync-manager.js
@@ -1,6 +1,7 @@
 /* global API msg */// msg.js
 /* global chromeLocal */// storage-util.js
 /* global compareRevision */// common.js
+/* global iconMan */
 /* global prefs */
 /* global tokenMan */
 'use strict';
@@ -99,8 +100,10 @@ const syncMan = (() => {
         }
         await syncMan.syncNow();
         status.errorMessage = null;
+        iconMan.overrideBadge();
       } catch (err) {
         status.errorMessage = err.message;
+        iconMan.overrideBadge({text: 'x', color: '#F00'});
         // FIXME: should we move this logic to options.js?
         if (!fromPref) {
           console.error(err);
diff --git a/background/tab-manager.js b/background/tab-manager.js
index ca5b183d..0a1fea07 100644
--- a/background/tab-manager.js
+++ b/background/tab-manager.js
@@ -53,6 +53,7 @@ const tabMan = (() => {
       }
     },
 
+    /** @returns {IterableIterator<number>} */
     list() {
       return cache.keys();
     },