Add Bluesky replies, quotes, and reposts to any web page. Handy for adding a comments section anywhere.
JavaScript 87.8%
HTML 12.2%
16 1 0

Clone this repository

https://tangled.org/jimray.bsky.team/bsky-conversation https://tangled.org/did:plc:lysqukqdu6hsrhet5v2brjgo/bsky-conversation
git@tangled.org:jimray.bsky.team/bsky-conversation git@tangled.org:did:plc:lysqukqdu6hsrhet5v2brjgo/bsky-conversation

For self-hosted knots, clone URLs may differ based on your setup.

Download tar.gz
README.md

<bsky-conversation>#

A zero-dependency web component that displays a Bluesky conversation thread — replies, quote posts, and reposts — for any public Bluesky post. Drop it into any page with a single <script> tag.

See it live on the atproto.com blog or a demo.

Quick start#

<script type="module" src="https://unpkg.com/bsky-conversation"></script>

<bsky-conversation uri="https://bsky.app/profile/did:plc:.../post/..."></bsky-conversation>

That's it. No build step, no dependencies.

Attributes#

Attribute Default Description
uri (required) The bsky.app post URL. Use DID-based URLs for reliability.
max-depth 3 How many levels of nested replies to show. At the cutoff, a "More of the conversation on Bluesky" link appears.
show-original-post false Set to "true" to include the root post in the timeline.
engage-text "Add your thoughts on Bluesky" CTA link text shown in the header and at the bottom. Set to "" to hide.
header-template (none) Custom header template string. Overrides the default header format.

Template syntax#

The header-template attribute supports a mini template language to let you customize how you introduce the conversation on your site.

Simple tokens — replaced with their value:

Token Value
{replies} Raw reply count
{quotes} Raw quote count
{reposts} Raw repost count
{repostedBy} Linked names, e.g. @alice, @bob, and 3 others
{postUrl} The bsky.app post URL

Pluralization{name|singular|plural} outputs nothing when 0, "1 singular" when 1, "N plural" when 2+:

{replies|reply|replies}        → "" or "1 reply" or "17 replies"
{quotes|quote|quotes}          → "" or "1 quote" or "5 quotes"

Conditional blocks{name?content} renders content only if the value is truthy:

{repostedBy?Reposted by {repostedBy}.}    → "" or "Reposted by @alice, @bob."
{replies?{replies|reply|replies} so far}   → "" or "17 replies so far"

Full example:

<bsky-conversation
  uri="https://bsky.app/profile/did:plc:.../post/..."
  header-template="This post has {replies?{replies|reply|replies}}{quotes?, {quotes|quote|quotes}}{repostedBy?, and has been reposted by {repostedBy}}."
/>

Styling timeline items#

The component assigns distinct CSS classes to each type of timeline item, so you can style replies, threaded replies, and quote posts independently:

Selector What it matches
.bsky-conversation .reply A direct reply to the original post
.bsky-conversation .thread .reply A nested reply within a thread (child of another reply)
.bsky-conversation .quote A quote post
.bsky-conversation .original The original post (when show-original-post="true")
.bsky-conversation .thread The <ol> wrapper around nested replies — has a left border by default

For example, to visually distinguish quote posts from replies:

bsky-conversation .quote {
  background: #f8f8fa;
  border-radius: 8px;
  padding: 1em;
}

CSS custom properties#

The component defines design tokens with sensible defaults, overridable from the host page. All internal sizing uses em units, so it scales with inherited font size.

Property Light default Dark default Controls
--bsky-border-color #e5e7eb #374151 Separators, thread lines
--bsky-muted-color #6b7280 #9ca3af Handles, timestamps, secondary text
--bsky-link-color black #60a5fa Link text color
--bsky-link-hover #2563eb #3b82f6 Link hover color
--bsky-link-underline rgba(82,82,91,0.5) rgba(59,130,246,0.3) Link underline color
--bsky-link-underline-hover rgba(59,130,246,0.3) rgba(59,130,246,0.3) Link underline hover color

Override example:

bsky-conversation {
  --bsky-link-color: #333;
  --bsky-muted-color: #888;
}

Dark mode#

The component ships with a built-in dark palette that activates when any ancestor has the dark class. This works with common dark mode patterns:

<!-- Toggle dark class on <html> or <body> -->
<html class="dark">
  <body>
    <bsky-conversation uri="..."></bsky-conversation>
  </body>
</html>

If your site uses a different convention (e.g., data-theme="dark" or prefers-color-scheme), you can set the properties yourself:

@media (prefers-color-scheme: dark) {
  bsky-conversation {
    --bsky-border-color: #374151;
    --bsky-muted-color: #9ca3af;
    --bsky-link-color: #60a5fa;
    --bsky-link-underline: rgba(59, 130, 246, 0.3);
    --bsky-link-hover: #3b82f6;
    --bsky-link-underline-hover: rgba(59, 130, 246, 0.3);
  }
}

Behavior#

  • Fetches directly from the public Bluesky API (no authentication needed)
  • Rich text rendering with proper UTF-8 byte-offset facet handling (links, @mentions, #hashtags)
  • Root post author's direct replies are filtered out (they're extensions of the original post, not conversation)
  • Hidden replies (via threadgate) and detached quotes (via postgate) are filtered out
  • Reply threads stay grouped — nested replies are not flattened into the timeline
  • Quote posts are interleaved chronologically with top-level reply threads
  • Reposts appear only in the header summary, not as timeline items
  • If quote or repost API calls fail, the thread still renders without that data. If the thread itself cannot be fetched, the component renders nothing.
  • All user content is XSS-hardened through escapeHtml()

Moderation#

Sometimes people will reply or repost in a manner you don't want to appear on your site. You can use Bluesky's built in tools to help you manage this.

To hide a reply, go to the post on bsky.app, select "Hide reply from everyone" from the ellipsis menu, and it will no longer appear on the page.

If someone quotes your post and you detach that quote, it will no longer appear in the conversation.

Additionally, this package includes an optional script that takes the full URL of a post that you wish to hide. In order for this to work, you will need to create a .env file and include your bsky handle and an app password.

The hide-reply script lets you hide replies or detach quote posts from conversations via the command line. It auto-detects the post type.

Setup#

cp .env.example .env

Fill in your ATPROTO_HANDLE and ATPROTO_APP_PASSWORD (create an app password in Bluesky Settings > App Passwords). Then install the dev dependencies:

npm install

Usage#

# Hide a reply (adds to threadgate hiddenReplies)
npm run hide-reply https://bsky.app/profile/did:plc:.../post/...

# Detach a quote post (adds to postgate detachedEmbeddingUris)
npm run hide-reply https://bsky.app/profile/did:plc:.../post/...

The script auto-detects whether the URL is a reply or a quote post:

  • Replies: Walks up the thread to find the root post and adds the reply URI to the root post's app.bsky.feed.threadgate record. Equivalent to "Hide reply for everyone" on bsky.app.
  • Quote posts: Detects the embedded post and adds the quote URI to the root post's app.bsky.feed.postgate record. Equivalent to "Detach quote" on bsky.app.

The authenticated user must own the root post being replied to or quoted.

License#

MIT