- TypeScript 75.9%
- CSS 15.9%
- JavaScript 6.7%
- HTML 1.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| entrypoints | ||
| public | ||
| scripts | ||
| utils | ||
| .gitignore | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| wxt.config.ts | ||
Reddit Inline Comments
VIBE CODING WARNING This extension was built with Deepseek v4.1 Flash
A Firefox extension (built with WXT) that adds a Comments button to every post in Reddit's feed and expands the thread inline, right under the post. No permalink, no page load, no losing your place in the feed.
┌──────────────────────────────────────────────────────────────┐
│ r/webdev · u/alice · 3h Some title... │ ← compact feed row
│ 💬 Comments 12 │ ← injected toggle
├──────────────────────────────────────────────────────────────┤
│ Showing 42 comments [Best][Top][New][Controversial][Old] ↻ ✕│ ← injected panel
│ │
│ ▾ u/alice OP ▲ 15 ▼ 1h ↗ │
│ Hello world. │
│ ▾ u/bob MOD ▲ 3 ▼ 2m ↗ │
│ A reply │
│ ↳ 2 more replies (deep-links to the thread) │
└──────────────────────────────────────────────────────────────┘
▲/▼ vote on a comment without leaving the feed, and ↗ opens that
comment's permalink in a new tab.
Requirements
- Firefox 142+ (desktop)
- Node 20+ and npm, only for development
Quick start
npm install
npm run dev # builds and launches Firefox with the extension loaded
Then browse https://www.reddit.com/ and click a Comments chip.
To produce an installable build:
npm run build # -> .output/firefox-mv2/
npm run zip # -> .output/reddit-inline-comments-0.1.0-firefox.zip
For a permanent install, load the zip via about:debugging#/runtime/this-firefox
→ Load Temporary Add-on, or sign it on
addons.mozilla.org.
Settings
Click the toolbar icon to pick the default comment sort — the sort a thread starts with. It defaults to Top.
The in-feed sort switcher writes to the same setting, so choosing New in a
panel also makes New the default. The popup, an open feed, and any other
context stay in step through storage.onChanged, and a new default applies to
the threads you open afterwards without a page reload.
How it works
content script (page) background script (extension page)
───────────────────── ──────────────────────────────────
1. find <shreddit-post> in the feed
2. inject a shadow-DOM host
as a sibling of the post
3. on click ──── rfe:fetch-thread ────▶ 4. GET /comments/{id}.json?raw_json=1
5. normalise the comment tree
7. render into the shadow root ◀────── 6. reply (cached 5 min, max 3 at once)
Three things are worth calling out.
Fetching happens in the background script. That keeps the page's Content-Security-Policy out of the picture, sends the request with the extension's host permissions (and the user's Reddit cookies), and lets fetches be cached and rate-limited centrally instead of once per tab.
The UI is a sibling of <shreddit-post>, not a child. Reddit renders an
absolutely-positioned overlay link (<a slot="full-post-link" class="absolute inset-0">) across the whole post, so anything inside the element would navigate
when clicked.
Every panel gets its own shadow root. Reddit's styles cannot leak in and ours cannot leak out, while inherited CSS custom properties still flow through — which is why the panel follows Reddit's light/dark theme for free.
Voting
Reads go through the background script, but writes happen from the content
script (entrypoints/reddit.content/vote.ts), because two of the three things
a vote needs are only available there:
- The CSRF token is in a cookie.
csrf_tokenis a double-submit token, so the content script reads it fromdocument.cookie(falling back to themodhashfrom/api/me.json). A background page cannot see it. - URLs must be absolute. A content script's
fetchcannot resolve a relative URL — it throwsTypeError: /api/vote is not a valid URLand the request never leaves the browser.
Reddit's current client no longer uses the legacy /api/vote endpoint. It posts
a GraphQL operation, and that is exactly what we send:
POST https://www.reddit.com/svc/shreddit/graphql
{"operation":"UpdateCommentVoteState",
"variables":{"input":{"commentId":"t1_ph21u80","voteState":"UP"}},
"csrf_token":"<csrf_token>"}
voteState is UP, DOWN or NONE — clicking the arrow you already picked
clears it. The UI updates optimistically and rolls back with a short notice if
Reddit refuses: a GraphQL errors[0].extensions.code, an HTTP 403/429, and an
unreadable token each get their own message.
The transport was verified against a real headless Firefox (MV2) rather than
assumed. An absolute-URL fetch from a content script to a host the extension
has permission for does carry the session cookies, SameSite=Strict ones
included, and a JSON Content-Type does not trigger a CORS preflight.
Comment permalinks use Reddit's own shape,
/r/{sub}/comments/{postId}/comment/{commentId}/, which is also what the
“N more replies” links target.
Reddit's DOM contract
Everything Reddit-specific lives in one file:
entrypoints/reddit.content/posts.ts. The shape it expects (captured from a
real compact feed):
<shreddit-feed reload-url="...feedViewType=compactView...">
<article class="w-full m-0" data-post-id="t3_1x72w5i">
<shreddit-post
id="t3_1x72w5i"
view-type="compactView"
feedindex="0"
permalink="/r/SteamDeck/comments/1x72w5i/slug/"
comment-count="10"
post-title="..."
subreddit-prefixed-name="r/SteamDeck"
>
<a slot="full-post-link" class="absolute inset-0" href="..."></a>
</shreddit-post>
<faceplate-partial ...></faceplate-partial>
</article>
<hr />
<shreddit-feed></shreddit-feed
></shreddit-feed>
A post qualifies if it has a t3_… id, is not promoted, is not inside a
comment, and is inside a feed (shreddit-feed, an article[data-post-id], or
has feedindex). That last rule is what keeps the button off the big hero post
on permalink pages. If Reddit reshuffles their markup, that is the one file to
edit.
Comment data
Comments come from Reddit's public JSON endpoint — the same payload the thread page hydrates from, no API key or OAuth needed:
https://www.reddit.com/comments/{id}.json?raw_json=1&sort=confidence&limit=100&depth=6
limit and depth cap one request (see DEFAULT_LIMIT / DEFAULT_DEPTH in
entrypoints/background.ts). Replies Reddit holds back appear as a
“N more replies” link that deep-links to the thread rather than paginating
inline. Results are cached for five minutes in the background and again per
panel; the ↻ button sends force to skip both, so it always refetches.
Each comment's likes field (true upvoted, false downvoted, null nothing)
seeds its vote control, so the arrows reflect how you have already voted.
Sanitising
body_html is attacker-controlled text from the network, so it is re-sanitised
before it is inserted (entrypoints/reddit.content/sanitize.ts): tag and
attribute allowlists, on*/style stripped, <script>/<iframe>/<svg>/…
dropped, javascript: URLs unwrapped to plain text, images restricted to
Reddit's own media hosts, and surviving links forced to
target="_blank" rel="noopener noreferrer nofollow". Parsing happens inside an
inert <template>, so nothing executes or loads during the pass.
Nothing is collected or transmitted anywhere except the comment requests you trigger and the votes you cast (which Reddit attributes to your own signed-in session). The only local state is your default comment sort.
Project layout
wxt.config.ts manifest, permissions, Firefox settings
entrypoints/
background.ts fetch + cache + rate limit + messaging
popup/
index.html the toolbar settings popup
main.ts builds the sort options from shared constants
popup.css
reddit.content/
index.ts feed scanning, MutationObserver, lifecycle
panel.ts the injected shadow-DOM UI
posts.ts all Reddit DOM knowledge
vote.ts comment up/down votes (same-origin writes)
sanitize.ts strict HTML allowlist
format.ts numbers, relative times, Reddit URLs
styles.css injected into each shadow root
utils/
types.ts message + data contracts
settings.ts the persisted default sort, shared by popup + feed
reddit-thread.ts pure Reddit JSON -> ThreadData translation
scripts/
generate-icons.mjs dependency-free PNG icon generator
Limitations
- Desktop only. Firefox for Android serves a different layout; the selectors here are for the desktop compact feed.
old.reddit.comis not supported and is deliberately excluded frommatchesinentrypoints/reddit.content/index.ts— it uses unrelated markup, so the extension would need a second adapter.- Long threads are partly linked out. Reddit truncates responses; the "N more replies" link opens the thread instead of fetching again.
- Rate limits. Reddit returns
429if you open posts very quickly; the panel says so and offers a Retry. Being signed in to reddit.com helps. - Voting needs a signed-in reddit.com session, and is cast as that account.
The request mirrors Reddit's own client (
UpdateCommentVoteStateagainst/svc/shreddit/graphql) and the extension-to-Reddit transport is verified against headless Firefox, but the round trip has never been exercised against a live logged-in Reddit from this environment. If Reddit reshapes its client, this is the path most likely to need updating. more-type pagination and non-feed surfaces (e.g. search result cards with a different container) are not special-cased.