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).
- Copy the entire single line in the code block below.
- Create a new bookmark in your browser.
- Paste the line into the bookmark's URL / Location field (not the name).
- Click the bookmark from anywhere.
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')})();
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.
(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');
})();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.