Skip to content

Instantly share code, notes, and snippets.

@lacymorrow
Last active June 23, 2026 18:04
Show Gist options
  • Select an option

  • Save lacymorrow/8be7b1c450e4e43d17791177e7b25e4a to your computer and use it in GitHub Desktop.

Select an option

Save lacymorrow/8be7b1c450e4e43d17791177e7b25e4a to your computer and use it in GitHub Desktop.
X/Twitter — Block "Recommended" Notifications Bookmarklet

X/Twitter — Block "Recommended" Notifications Bookmarklet

X has no setting to disable algorithmic "Recommended post" notifications. This bookmarklet hides them on https://x.com/notifications while leaving real notifications (follows, likes, replies, mentions) untouched. It also blocks Community "New pinned post in …" notifications by matching their text, while leaving other Community notifications alone.

Visual feedback: each recommended row flashes a blue border and fades out before collapsing, and a small blue toast pill appears in the top-right showing how many were blocked (and "Recommendations restored" when you toggle it off).

It also navigates to the notifications page for you:

  • Already on /notifications → runs immediately.
  • On x.com but another page → clicks the in-app Notifications link (single-page navigation, so the script keeps running) and then runs.
  • Not on X at all → sends you to x.com/notifications. That's a full page load, which bookmarklets can't survive, so just click the bookmark once more once the page opens.

Run once to enable, run again to disable (toggle).

Install

  1. Copy the entire single line in the code block below.
  2. Create a new bookmark in your browser.
  3. Paste the line into the bookmark's URL / Location field (not the name).
  4. Click the bookmark from anywhere.

Bookmarklet

javascript:(function(){var S='M22.99',B=['New pinned post in'];function H(c){if(c.dataset.xrk==='1')return;c.dataset.xrk='1';c.style.transition='opacity .35s ease, box-shadow .35s ease';c.style.boxShadow='inset 0 0 0 2px #1d9bf0';c.style.opacity='0';setTimeout(function(){if(c.dataset.xrk==='1')c.style.display='none'},350)}function V(c){if(c.dataset.xrk!=='1'&&c.style.display!=='none')return;delete c.dataset.xrk;c.style.display='';c.style.opacity='';c.style.boxShadow='';c.style.transition=''}function T(m){var t=document.createElement('div');t.textContent=m;t.style.cssText='position:fixed;top:16px;right:16px;z-index:2147483647;background:#1d9bf0;color:#fff;font:600 14px -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;padding:10px 14px;border-radius:9999px;box-shadow:0 4px 14px rgba(0,0,0,.25);opacity:0;transform:translateY(-8px);transition:opacity .25s,transform .25s;pointer-events:none';document.body.appendChild(t);requestAnimationFrame(function(){t.style.opacity='1';t.style.transform='translateY(0)'});setTimeout(function(){t.style.opacity='0';t.style.transform='translateY(-8px)';setTimeout(function(){t.remove()},300)},2200)}if(window.__xRecKiller){window.__xRecKiller.obs.disconnect();document.querySelectorAll('[data-testid="cellInnerDiv"]').forEach(V);delete window.__xRecKiller;T('Recommendations restored');return}function R(a){var p=a.querySelector('svg path'),d=p&&p.getAttribute('d');if(d&&d.indexOf(S)===0)return true;var x=(a.textContent||'').toLowerCase();return B.some(function(b){return x.indexOf(b.toLowerCase())!==-1})}function W(){document.querySelectorAll('article[data-testid="notification"]').forEach(function(a){var c=a.closest('[data-testid="cellInnerDiv"]')||a;if(R(a))H(c);else V(c)})}var s=false;function n(){if(s)return;s=true;requestAnimationFrame(function(){s=false;W()})}var h=location.hostname;if(!/(^|\.)x\.com$|(^|\.)twitter\.com$/.test(h)){location.href='https://x.com/notifications';return}if(!/^\/notifications/.test(location.pathname)){var l=document.querySelector('a[href="/notifications"][role="link"]')||document.querySelector('a[href="/notifications"]');if(l){l.click()}else{location.href='/notifications'}}var k=0;document.querySelectorAll('article[data-testid="notification"]').forEach(function(a){if(R(a))k++});var o=new MutationObserver(n);o.observe(document.body,{childList:true,subtree:true});window.__xRecKiller={obs:o};W();T(k?('Blocked '+k+' recommended post'+(k===1?'':'s')):'Blocking recommended posts')})();

How it detects them

Each notification row has a leading SVG icon. The icon's path d attribute is the reliable signal — more robust than matching the "Recent post from" text, because some recommended posts show only a name + tweet with no such label.

Type Path prefix
Recommended (hide) M22.99… sparkle
Like (keep) M20.884… heart
Follow (keep) M17.863… person

Some rows can't be told apart by icon. Community "New pinned post in …" alerts use the generic Communities person icon, which is shared with other community notifications you want to keep — so those are matched by text instead: a row is also hidden if its textContent contains any phrase in a case-insensitive block list (currently just New pinned post in). The list is an array, so more phrases can be added later. In short, a row is hidden if its icon path starts with M22.99 or its text matches a blocked phrase.

The timeline is virtualized: cells are recycled as you scroll, so a MutationObserver re-runs on every change and toggles visibility both ways (hide recommended, re-show anything else) to avoid a recycled cell getting stuck hidden. That same observer is what survives the in-app navigation.

The flash uses box-shadow + opacity only — never transform, which X relies on to position each virtualized row.

Readable source

(function () {
  var STAR = 'M22.99'; // recommended-post sparkle icon path prefix (no space = paste-safe)
  var BLOCKED_TEXT = ['New pinned post in']; // case-insensitive phrases; extend as needed
  var BLUE = '#1d9bf0';

  function hideCell(c) {
    if (c.dataset.xrk === '1') return;              // already hidden/animating
    c.dataset.xrk = '1';
    c.style.transition = 'opacity .35s ease, box-shadow .35s ease';
    c.style.boxShadow = 'inset 0 0 0 2px ' + BLUE;  // brief blue flash (no transform!)
    c.style.opacity = '0';
    setTimeout(function () {
      if (c.dataset.xrk === '1') c.style.display = 'none';
    }, 350);
  }

  function showCell(c) {
    if (c.dataset.xrk !== '1' && c.style.display !== 'none') return;
    delete c.dataset.xrk;
    c.style.display = '';
    c.style.opacity = '';
    c.style.boxShadow = '';
    c.style.transition = '';
  }

  function toast(msg) {
    var t = document.createElement('div');
    t.textContent = msg;
    t.style.cssText =
      'position:fixed;top:16px;right:16px;z-index:2147483647;background:' + BLUE +
      ';color:#fff;font:600 14px -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;' +
      'padding:10px 14px;border-radius:9999px;box-shadow:0 4px 14px rgba(0,0,0,.25);' +
      'opacity:0;transform:translateY(-8px);transition:opacity .25s,transform .25s;pointer-events:none';
    document.body.appendChild(t);
    requestAnimationFrame(function () {
      t.style.opacity = '1';
      t.style.transform = 'translateY(0)';
    });
    setTimeout(function () {
      t.style.opacity = '0';
      t.style.transform = 'translateY(-8px)';
      setTimeout(function () { t.remove(); }, 300);
    }, 2200);
  }

  // Toggle off if already running
  if (window.__xRecKiller) {
    window.__xRecKiller.obs.disconnect();
    document.querySelectorAll('[data-testid="cellInnerDiv"]').forEach(showCell);
    delete window.__xRecKiller;
    toast('Recommendations restored');
    return;
  }

  function isRecommended(a) {
    var p = a.querySelector('svg path');
    var d = p && p.getAttribute('d');
    if (d && d.indexOf(STAR) === 0) return true;            // sparkle icon → recommended
    var text = (a.textContent || '').toLowerCase();         // otherwise match by text
    return BLOCKED_TEXT.some(function (phrase) {
      return text.indexOf(phrase.toLowerCase()) !== -1;
    });
  }

  function sweep() {
    document.querySelectorAll('article[data-testid="notification"]').forEach(function (a) {
      var c = a.closest('[data-testid="cellInnerDiv"]') || a;
      if (isRecommended(a)) hideCell(c);
      else showCell(c);
    });
  }

  var scheduled = false;
  function schedule() {
    if (scheduled) return;
    scheduled = true;
    requestAnimationFrame(function () { scheduled = false; sweep(); });
  }

  // Not on X at all → full navigation (bookmarklet can't survive a reload).
  var host = location.hostname;
  if (!/(^|\.)x\.com$|(^|\.)twitter\.com$/.test(host)) {
    location.href = 'https://x.com/notifications';
    return;
  }

  // On X but not on the notifications route → trigger the in-app link (SPA nav).
  if (!/^\/notifications/.test(location.pathname)) {
    var link =
      document.querySelector('a[href="/notifications"][role="link"]') ||
      document.querySelector('a[href="/notifications"]');
    if (link) link.click();
    else location.href = '/notifications';
  }

  var count = 0;
  document.querySelectorAll('article[data-testid="notification"]').forEach(function (a) {
    if (isRecommended(a)) count++;
  });

  var obs = new MutationObserver(schedule);
  obs.observe(document.body, { childList: true, subtree: true });
  window.__xRecKiller = { obs: obs };
  sweep();
  toast(count
    ? 'Blocked ' + count + ' recommended post' + (count === 1 ? '' : 's')
    : 'Blocking recommended posts');
})();

Want it fully automatic, even from a cold tab?

A bookmarklet can't run after a full page reload. If you'd rather it just always work without clicking, drop the Readable source above into a Tampermonkey / Violentmonkey userscript with:

// @match https://x.com/notifications*
// @match https://twitter.com/notifications*

and it will auto-run every time you open the notifications page.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment