DEV Community

ayumu kitabayashi
ayumu kitabayashi

Posted on Edited on

Hiding Instagram Reels inside a WKWebView: CSS fails silently, so count it

minulo is an iPhone app for people who want to reply to Instagram DMs without getting pulled into Reels: it opens Instagram inside a WKWebView and removes the Reels entry points with CSS. This post explains why that CSS fails silently and how counting each selector's matches catches it. (I'm a solo developer in Japan, and I build minulo.)

The idea is simple. Instagram, YouTube and a few others open inside a WKWebView, and CSS removes the Reels/Shorts entry points. The official apps are blocked with Screen Time (FamilyControls / ManagedSettings), so opening them sends you to minulo instead.

Here are three things that were harder than I expected.

1. Inject twice. CSS goes in injectedJavaScriptBeforeContentLoaded, otherwise the Reels tab flashes before it disappears. JS rules and a MutationObserver go in injectedJavaScript. These sites are SPAs, so URL-scoped rules get re-applied on pushState/replaceState/popstate.

I once wrapped history.pushState inside the function that runs on every DOM change. Every run added another wrapper, so the longer the page stayed open, the slower it got, until it froze. Wrap once.

2. display:none can blank the whole feed. Hiding suggested posts with display:none stopped Instagram from rendering anything below them. The feed is virtualized, and a zero-height item seems to tell it there's nothing more to draw. I reproduced it 3/3 in the simulator. Now I tag the element and keep its height.

Also, a[href^="/reel"] looked like it hid the profile Reels tab. It only hid the icon. The link was still tappable, because that URL is /<username>/reels/. a[href$="/reels/"] fixed it.

3. CSS doesn't throw. When a site changes its markup, the selector simply stops matching. Nothing errors, so try/catch won't catch it. Every 30 seconds, each rule's selector is run through querySelectorAll and the match counts get posted back to the app. A rule that stays at zero is treated as broken. Two caveats: skip counting on login pages (everything is zero there), and only count URL-scoped rules on their own URLs.

Bonus: opening a URL from a ShieldAction extension is silently ignored on a real device. My workaround: the shield button posts a local notification, and tapping it opens minulo. The blocked app's name is only known inside the extension, so I put it in the URL query, percent-encoded. With a space in the name, URL(string:) returns nil and nothing happens.

Hiding is free: https://apps.apple.com/app/id6809352229

If you're not a developer and just want the how-to: how to block Instagram Reels on iPhone and keep DMs, and a comparison of the apps that do this.

Top comments (0)