
It calls the AT Protocol’s public HTTP API with native fetch to retrieve thread data and renders the output as semantic HTML in the Light DOM.
The component registers as the <bsky-comments> custom element and works in plain HTML files and in framework projects including React, Vue, Svelte, and Astro.
Features
- Roughly 3 kB gzipped total size with no external SDK bundled.
- Accepts a standard public Bluesky post URL or a raw AT-URI as the source.
- Renders all output in the Light DOM for direct CSS control.
- Ascending and descending sort order.
- Rich text support for mentions, hashtags, and inline links using AT Protocol facet data.
- Customizable like and reply icons via HTML attributes or CSS pseudo-elements.
- Configurable PDS endpoint for self-hosted Bluesky instances.
- Lazy-loaded avatar images to reduce initial page load impact.
- Recursive rendering of nested reply threads up to a configurable depth.
- Cancels in-flight fetch requests on element disconnect to prevent stale state overwrites.
How to use it:
1. Install & download.
# NPM $ npm install bsky-comments
2. Load the component from a CDN:
<script type="module" src="https://unpkg.com/bsky-comments"></script>
3. Add the <bsky-comments> component to your webpage and paste the URL directly from your browser. The component resolves the handle to a DID automatically.
<!-- The post attribute accepts any standard bsky.app post URL --> <bsky-comments post="https://bsky.app/profile/yourhandle.bsky.social/post/abc123xyz456" ></bsky-comments>
4. When your build process already knows the DID, pass the AT-URI via the `uri` attribute. This skips the handle-resolution API call entirely.
<!-- uri takes precedence over post when both are set --> <!-- Use this on static sites where the DID is known at build time --> <bsky-comments uri="at://did:plc:exampleDID12345/app.bsky.feed.post/abc123xyz456" ></bsky-comments>
5. Show newest comments first:
<bsky-comments post="https://bsky.app/profile/yourhandle.bsky.social/post/abc123xyz456" sort="desc" ></bsky-comments>
6. Pass raw emoji or inline SVG directly into the icon attributes:
<!-- Replace the default heart and chat-bubble icons with emoji --> <bsky-comments post="https://bsky.app/profile/yourhandle.bsky.social/post/abc123xyz456" icon-like="💙" icon-reply="↩️" ></bsky-comments> <!-- Or use inline SVG for pixel-perfect control --> <bsky-comments post="https://bsky.app/profile/yourhandle.bsky.social/post/abc123xyz456" icon-like='<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5A5.5 5.5 0 0 1 7.5 3c1.74 0 3.41.81 4.5 2.09A6.01 6.01 0 0 1 16.5 3 5.5 5.5 0 0 1 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>' ></bsky-comments>
/* Hide the default emoji and replace it with a background image */
.bsky-icon-like {
display: inline-block;
width: 16px;
height: 16px;
color: transparent; /* Makes the default emoji invisible */
background-image: url('/icons/heart.svg');
background-size: contain;
background-repeat: no-repeat;
}7. All available props for the <bsky-comments> component.
-
post(string): The public web URL of the Bluesky post (e.g.,https://bsky.app/profile/...). The component resolves the handle to a DID automatically via one extra API call. -
uri(string): The internal AT-URI (e.g.,at://did:plc:...). When set, this takes precedence overpostand skips handle resolution. -
sort(string): Comment sort order. Acceptsasc(oldest first, default) ordesc(newest first). -
service(string): The PDS API base URL. Defaults topublic.api.bsky.app. Set this for self-hosted Bluesky instances. -
icon-like(string): Custom HTML string or emoji for the like icon. Defaults to❤️. -
icon-reply(string): Custom HTML string or emoji for the reply icon. Defaults to💬. -
depth(number): Maximum nesting depth for fetched replies. Defaults to10.
8. Customize the component with the following CSS classes:
/* Add a left border and bottom spacing to each comment */
.bsky-comment {
border-left: 2px solid #e5e7eb;
padding-left: 1rem;
margin-bottom: 1.25rem;
}
/* Style the actions row for likes and replies */
.bsky-actions {
display: flex;
gap: 1rem;
font-size: 0.875rem;
color: #6b7280;
margin-top: 0.5rem;
}
/* Indent nested reply threads */
.bsky-replies {
margin-top: 0.75rem;
padding-left: 1rem;
}9. Or via TailwindCSS:
<!-- Wrap the component and target its internal classes with Tailwind selectors --> <div class=" [&_.bsky-comment]:border-l-2 [&_.bsky-comment]:border-gray-200 [&_.bsky-comment]:pl-4 [&_.bsky-comment]:mb-4 [&_.bsky-actions]:text-sm [&_.bsky-actions]:text-gray-500 [&_.bsky-actions]:flex [&_.bsky-actions]:gap-4 "> <bsky-comments post="https://bsky.app/profile/yourhandle.bsky.social/post/abc123xyz456" /> </div>
Framework Integration
React / Next.js:
// Import once to register the custom element globally
import 'bsky-comments';
export function PostPage() {
return (
<section className="comments-section">
{/* Use it like any other HTML element in JSX */}
<bsky-comments post="https://bsky.app/profile/yourhandle.bsky.social/post/abc123xyz456" />
</section>
);
}
Framework Integration:
Vue / Nuxt:
<script setup> // Registering the custom element on component mount import 'bsky-comments'; </script> <template> <!-- Bind the post URL from your reactive state --> <bsky-comments :post="currentPostUrl" /> </template>
Svelte / SvelteKit:
<script> // Import at the top of your layout or page file import 'bsky-comments'; </script> <!-- The component renders as a standard HTML element --> <bsky-comments post="https://bsky.app/profile/yourhandle.bsky.social/post/abc123xyz456" />
Astro:
--- // Import inside the frontmatter block for server-side bundling import 'bsky-comments'; --- <!-- Pass a static post URL resolved at build time --> <bsky-comments post="https://bsky.app/profile/yourhandle.bsky.social/post/abc123xyz456" />
Alternatively, use the CDN import in an Astro <script> tag:
--- --- <bsky-comments post="https://bsky.app/profile/yourhandle.bsky.social/post/abc123xyz456" /> <script> // Loads the component at runtime on the client side import 'bsky-comments'; </script>
FAQs:
Q: What is the difference between the post and uri attributes?
A: post accepts a standard https://bsky.app/... URL and resolves the handle to a DID via one extra API call. uri accepts a pre-resolved at://did:plc:... string and skips that step. Use uri on static sites where the DID is available at build time to cut the request count in half.
Q: Why am I seeing a “Could not resolve handle” error?
A: The handle in the post URL no longer resolves on the public Bluesky network, usually because the account was renamed or deleted. Pass the uri attribute with the full AT-URI to bypass handle resolution.
Q: Does Tailwind CSS work with this component?
A: Yes. The component renders in the Light DOM, so Tailwind’s arbitrary variant selectors (e.g., [&_.bsky-comment]:border-l-2) apply directly on the wrapper element.
Q: Can I point the component at a self-hosted Bluesky instance?
A: Yes. Set the service attribute to your PDS base URL (e.g., service="https://pds.example.com/xrpc"). The component sends all API requests to that endpoint.
The post Embed Bluesky Comment Threads Anywhere With bsky-comments appeared first on CSS Script.
Discover more from RSS Feeds Cloud
Subscribe to get the latest posts sent to your email.
