Components to display (and encourage) likes on BlueSky posts.
-
<bluesky-likes>
: Displays the number of likes on a post. -
<bluesky-likers>
: Displays avatars of users who liked a post.
For a demo, check out https://projects.verou.me/bluesky-likes/
Can be used separately, or together.
E.g. for something similar to Salma Alam-Nayor’s:
<h2>
<bluesky-likes src="https://bsky.app/profile/lea.verou.me/post/3lhygzakuic2n"></bluesky-likes>
likes on Bluesky
</h2>
<p>
<a href="https://bsky.app/profile/lea.verou.me/post/3lhygzakuic2n">Like this post on Bluesky to see your face on this page</a>
</p>
<bluesky-likers src="https://bsky.app/profile/lea.verou.me/post/3lhygzakuic2n"></bluesky-likers>
These components are designed to make common cases easy, and complex cases possible.
- Dynamic: Components respond to changes in the URL of the post — or when it’s lazily set later
- Ultra-lightweight: The whole package is ~2 KB minified & gzipped and dependency-free
- Accessible & i18n friendly
- Autoloading is available to take the hassle out of figuring out when to load the components
- Highly customizable styling via regular CSS properties, custom properties, states, and parts (but also beautiful by default so you don’t have to).
- Customizable content via slots
- Hackable: You can replace the templates and styles of the components with your own, or even subclass them to create new components with different templates and styles
The easiest way is to use the autoloader and a CDN such as unpkg. All it takes is pasting this into your HTML and you’re ready to use the components:
<script src="https://unpkg.com/bluesky-likes/autoload" type="module"></script>
Or, if you know which ones you need, you can import them individually:
<script src="https://unpkg.com/bluesky-likes/likes" type="module"></script>
<script src="https://unpkg.com/bluesky-likes/likers" type="module"></script>
You can also install the components via npm and use with your toolchain of choice:
npm install bluesky-likes
Then import the components in your JavaScript. You can import everything:
import { BlueskyLikes, BlueskyLikers, bsky } from "bluesky-likes";
Or you can use individual exports like bluesky-likes/likes
.
Displays the number of likes on a post and links to the full list.
<bluesky-likes src="https://bsky.app/profile/lea.verou.me/post/3lhygzakuic2n"></bluesky-likes>
To link to a different URL (e.g. the post itself), simply wrap the component in a link:
<a href="https://bsky.app/profile/lea.verou.me/post/3lhygzakuic2n">
<bluesky-likes src="https://bsky.app/profile/lea.verou.me/post/3lhygzakuic2n"></bluesky-likes>
</a>
Attribute | Type | Description |
---|---|---|
src |
string |
The URL of the post to display likes for. |
Name | Description |
---|---|
(Default) | Content added after the count. |
prefix |
Custom icon |
None!
Pretty much all styling is on the host element, so you can just override regular CSS properties such as border
, padding
or color
to restyle the component.
Name | Description |
---|---|
link |
The <a> element that links to all likes. |
count |
The <span> that contains the like count. |
icon |
The default icon which is displayed if nothing is slotted in the prefix slot. |
Name | Description |
---|---|
loading |
Indicates that the component is currently loading data. |
Displays the avatars of users who liked a post up to a max limit, and the number of additional users not shown.
<bluesky-likers src="https://bsky.app/profile/lea.verou.me/post/3lhygzakuic2n"></bluesky-likers>
| Attribute | Type | Description |
| --------- | -------- | --------------------------------------------------------- | --- |
| src
| string
| The URL of the post to display likes for. |
| max
| number
| The maximum number of avatars to display. Defaults to 50. | |
Name | Description |
---|---|
(Default) | Visually hidden content for screen reader users. See Accessibility Notes. |
empty |
Content displayed when there are no likers. |
- Apply
text-wrap: balance
to the component to equalize the width of the rows.
Name | Description |
---|---|
loading |
Indicates that the component is currently loading data. Note that the state will be removed when data loads and the component is updated, not after all avatars load. |
empty |
Indicates that there are no likers to display. |
Name | Default Value | Description |
---|---|---|
--avatar-size |
calc(2em + 1vw) |
The size of each avatar. |
--avatar-overlap-percentage |
0.3 |
The percentage of horizontal overlap between avatars. |
--avatar-overlap-percentage-y |
0.2 |
The percentage of vertical overlap between avatars. |
--avatar-border |
.15em solid canvas |
The border style for each avatar. |
--avatar-shadow |
0 .1em .4em -.3em rgb(0 0 0 / 0.4) |
The box-shadow applied to each avatar. |
--avatar-background |
url('data:image/svg+xml,…') center / cover canvas |
The background for avatars without a user image (default SVG, centered and covered). |
--more-background |
#1185fe |
The background color for the "+N" (more) avatar. |
--more-color-text |
white |
The text color for the "+N" (more) avatar. |
--avatar-overlap |
calc(var(--avatar-size) \* var(--avatar-overlap-percentage)) |
The actual horizontal overlap between avatars (as a <length> ). |
--avatar-overlap-y |
calc(var(--avatar-size) \* var(--avatar-overlap-percentage-y)) |
The actual vertical overlap between avatars (as a <length> ). |
Name | Description |
---|---|
avatar |
The circular element that displays a user, or the +N for users not shown. Corresponds to an <img> element for users with an avatar, and an <a> in other cases. |
avatar-img |
The <img> element for users with an avatar. |
link |
The <a> element that wraps each entry (either links to the user's profile, or to all likers) |
profile-link |
The <a> element that links to the user's profile. |
more |
The <a> element that displays the hidden count. |
Due to its side effects, the autoloader is a separate export:
import "bluesky-likes/autoload";
By default, the autoloader will not observe future changes: if the components are not available when the script runs, they will not be fetched. It will also not discover components that are in shadow roots of other components. This is done for performance reasons, since these features are slow and these components are mostly used on blogs and other content-focused websites that don’t need this.
If, however, you do, you can use the observe()
and discover()
methods the autoloader exports:
-
observe(root)
will observeroot
for changes and load components as they are added. You can useunobserve()
to stop observing. -
discover(root)
will discover components inroot
and load them if they are not already loaded.root
can be any DOM node, including documents and shadow roots.
For most common cases, slots should be sufficient for customizing the content of the components and regular CSS to for styling them. However, for more advanced use cases, you can completely gut them and replace their templates and styles with your own.
Every component class includes the templates used to render it as a static templates
property and its CSS styles as a styles
property.
For example, BlueskyLikes.templates
is the templates used by the <bluesky-likes>
component, and BlueskyLikers.styles
is the styles used by the <bluesky-likers>
component.
Each template is a function that takes a data
object and returns a string of HTML, while the styles are a string of CSS.
You can either tweak the templates directly, or you can create a subclass with different values and register it as a new component.
If you make changes after elements have already been initialized, you should call element.render({useCache: true})
on these elements.
Since these components had to interface with the BlueSky API, they also implement a tiny wrapper for the relevant parts of it.
While this library is absolutely not intended as a BlueSky API SDK, if you do need these functions, they are in src/api.js
and have their own export too: bluesky-likes/api
.
The following functions are available:
-
getProfile(handle)
: Fetches a user profile by handle. -
getPost(url)
: Fetches a post details by URL. -
getPostLikes(url)
: Fetches the likers for a post by its URL.
Also these, though you probably won’t need them unless you’re making new API calls not covered by these endpoints:
-
parsePostUrl(url)
: Parses a BlueSky post URL and returns the post's handle and URI. Synchronous. -
getDid(handle)
: Get the DID of a user by their handle. -
getPostUri(url)
: Fetches a post AT URI by its URL.
Unless otherwise mentioned, all functions are async.
These components are designed with accessibility in mind, in the sense that they use semantically appropriate HTML elements and have been tested with screen readers.
However, the accessibility of the end result also depends on how you use them.
By default, the icon’s alt text is empty, since it is considered presentational. To change this, you can slot in your own icon with a different alt text.
By default, the link’s title is "View all Bluesky likes". To localize this, you can wrap the element in another link, with your own title.
This component is intended to be used in a way that communicates its purpose to screen readers through context. If you just display a list of avatars with no other context, sighted users would be puzzled just as much. You can check out the example in the beginning for one possible way to do this.
One thing to note is that all avatars are wrapped in links that point to the user’s profile.
To prevent these links from trapping focus for keyboard users, they all have tabindex="-1"
,
otherwise keyboard users would have to hit Tab 101 times in the worst case to escape the component, which would be bad.
In the odd case where you’re using this component entirely by itself with no other link around it, you can provide content to screen readers via the default slot. This content will be visually hidden unless focused.
- Number formatting uses locale-aware formatting (via
Intl.NumberFormat
) using the element’s inherited language. - CSS uses logical properties where appropriate, so that the components can be used in right-to-left languages without changes
- Any content that may need to be localized has a way to replace it (e.g. via slots)
- Salma Alam-Nayor for the initial idea
- Dmitry Sharabin for the CodePen that sparked this
These components are MIT licensed. However, if you are using them in a way that helps you profit, there is a social (not legal) expectation that you give back by funding their development.