A React component library for rendering common AT Protocol records for applications such as Bluesky and Leaflet.
1# atproto-ui
2
3A React component library for rendering AT Protocol records (Bluesky, Leaflet, Tangled, and more). Handles DID resolution, PDS discovery, and record fetching automatically as well as caching these so multiple components can render quickly. [Live demo](https://atproto-ui.netlify.app).
4
5This project is mostly a wrapper on the extremely amazing work [Mary](https://mary.my.id/) has done with [atcute](https://tangled.org/@mary.my.id/atcute), please support it. I have to give thanks to [phil](https://bsky.app/profile/bad-example.com) for microcosm and slingshot. Incredible services being given for free that is responsible for why the components fetch data so quickly.
6
7## Screenshots
8
9
10
11
12## Features
13
14- **Drop-in components** for common record types (`BlueskyPost`, `BlueskyProfile`, `TangledRepo`, `LeafletDocument`)
15- **Prefetch support** - Pass data directly to skip API calls (perfect for SSR/caching)
16- **Caching** - Blobs, DIDs, and records are cached so components which use the same ones can render even quicker
17- **Customizable theming** - Override CSS variables to match your app's design
18- **Composable hooks** - Build custom renderers with protocol primitives
19- Built on lightweight [`@atcute/*`](https://tangled.org/@mary.my.id/atcute) clients
20
21## Installation
22
23```bash
24npm install atproto-ui
25```
26
27## Quick Start
28
29```tsx
30import { AtProtoProvider, BlueskyPost, LeafletDocument } from "atproto-ui";
31import "atproto-ui/styles.css";
32
33export function App() {
34 return (
35 <AtProtoProvider>
36 <BlueskyPost did="did:plc:example" rkey="3k2aexample" />
37 {/* You can use handles too */}
38 <LeafletDocument did="nekomimi.pet" rkey="3m2seagm2222c" />
39 </AtProtoProvider>
40 );
41}
42```
43
44**Note:** The library automatically imports the CSS when you import any component. If you prefer to import it explicitly (e.g., for better IDE support or control over load order), you can use `import "atproto-ui/styles.css"`.
45
46## Theming
47
48Components use CSS variables for theming. By default, they respond to system dark mode preferences, or you can set a theme explicitly:
49
50```tsx
51// Set theme via data attribute on document element
52document.documentElement.setAttribute("data-theme", "dark"); // or "light"
53
54// For system preference (default)
55document.documentElement.removeAttribute("data-theme");
56```
57
58### Available CSS Variables
59
60```css
61--atproto-color-bg
62--atproto-color-bg-elevated
63--atproto-color-text
64--atproto-color-text-secondary
65--atproto-color-border
66--atproto-color-link
67/* ...and more, check out lib/styles.css */
68```
69
70### Override Component Theme
71
72Wrap any component in a div with custom CSS variables to override its appearance:
73
74```tsx
75import { AtProtoStyles } from "atproto-ui";
76
77<div style={{
78 '--atproto-color-bg': '#f0f0f0',
79 '--atproto-color-text': '#000',
80 '--atproto-color-link': '#0066cc',
81} satisfies AtProtoStyles}>
82 <BlueskyPost did="..." rkey="..." />
83</div>
84```
85
86## Prefetched Data
87
88All components accept a `record` prop. When provided, the component uses your data immediately without making network requests. Perfect for SSR, caching, or when you've already fetched data.
89
90```tsx
91import { BlueskyPost, useLatestRecord } from "atproto-ui";
92import type { FeedPostRecord } from "atproto-ui";
93
94const MyComponent: React.FC<{ did: string }> = ({ did }) => {
95 // Fetch the latest post using the hook
96 const { record, rkey, loading } = useLatestRecord<FeedPostRecord>(
97 did,
98 "app.bsky.feed.post"
99 );
100
101 if (loading) return <p>Loading…</p>;
102 if (!record || !rkey) return <p>No posts found.</p>;
103
104 // Pass the fetched record directly—BlueskyPost won't re-fetch it
105 return <BlueskyPost did={did} rkey={rkey} record={record} />;
106};
107```
108
109All components support prefetched data:
110
111```tsx
112<BlueskyProfile did={did} record={profileRecord} />
113<TangledString did={did} rkey={rkey} record={stringRecord} />
114<LeafletDocument did={did} rkey={rkey} record={documentRecord} />
115```
116
117### Using atcute Directly
118
119Use atcute directly to construct records and pass them to components—fully compatible!
120
121```tsx
122import { Client, simpleFetchHandler, ok } from '@atcute/client';
123import type { AppBskyFeedPost } from '@atcute/bluesky';
124import { BlueskyPost } from 'atproto-ui';
125
126// Create atcute client
127const client = new Client({
128 handler: simpleFetchHandler({ service: 'https://public.api.bsky.app' })
129});
130
131// Fetch a record
132const data = await ok(
133 client.get('com.atproto.repo.getRecord', {
134 params: {
135 repo: 'did:plc:ttdrpj45ibqunmfhdsb4zdwq',
136 collection: 'app.bsky.feed.post',
137 rkey: '3m45rq4sjes2h'
138 }
139 })
140);
141
142const record = data.value as AppBskyFeedPost.Main;
143
144// Pass atcute record directly to component!
145<BlueskyPost
146 did="did:plc:ttdrpj45ibqunmfhdsb4zdwq"
147 rkey="3m45rq4sjes2h"
148 record={record}
149/>
150```
151
152## API Reference
153
154### Components
155
156| Component | Description |
157|-----------|-------------|
158| `AtProtoProvider` | Context provider for sharing protocol clients. Optional `plcDirectory` prop. |
159| `AtProtoRecord` | Core component for fetching/rendering any AT Protocol record. Accepts `record` prop. |
160| `BlueskyProfile` | Profile card for a DID/handle. Accepts `record`, `fallback`, `loadingIndicator`, `renderer`. |
161| `BlueskyPost` | Single Bluesky post. Accepts `record`, `iconPlacement`, custom renderers. |
162| `BlueskyQuotePost` | Post with quoted post support. Accepts `record`. |
163| `BlueskyPostList` | Paginated list of posts (default: 5 per page). |
164| `TangledString` | Tangled string (code snippet) renderer. Accepts `record`. |
165| `LeafletDocument` | Long-form document with blocks. Accepts `record`, `publicationRecord`. |
166
167### Hooks
168
169| Hook | Returns |
170|------|---------|
171| `useDidResolution(did)` | `{ did, handle, loading, error }` |
172| `useLatestRecord(did, collection)` | `{ record, rkey, loading, error, empty }` |
173| `usePaginatedRecords(options)` | `{ records, loading, hasNext, loadNext, ... }` |
174| `useBlob(did, cid)` | `{ url, loading, error }` |
175| `useAtProtoRecord(did, collection, rkey)` | `{ record, loading, error }` |
176
177## Advanced Usage
178
179### Using Hooks for Custom Logic
180
181```tsx
182import { useLatestRecord, BlueskyPost } from "atproto-ui";
183import type { FeedPostRecord } from "atproto-ui";
184
185const LatestBlueskyPost: React.FC<{ did: string }> = ({ did }) => {
186 const { record, rkey, loading, error, empty } = useLatestRecord<FeedPostRecord>(
187 did,
188 "app.bsky.feed.post",
189 );
190
191 if (loading) return <p>Fetching latest post…</p>;
192 if (error) return <p>Could not load: {error.message}</p>;
193 if (empty || !record || !rkey) return <p>No posts yet.</p>;
194
195 // Pass both record and rkey—no additional API call needed
196 return <BlueskyPost did={did} rkey={rkey} record={record} colorScheme="system" />;
197};
198```
199
200### Custom Renderer
201
202Use `AtProtoRecord` with a custom renderer for full control:
203
204```tsx
205import { AtProtoRecord } from "atproto-ui";
206import type { FeedPostRecord } from "atproto-ui";
207
208<AtProtoRecord<FeedPostRecord>
209 did={did}
210 collection="app.bsky.feed.post"
211 rkey={rkey}
212 renderer={({ record, loading, error }) => (
213 <article>
214 <strong>{record?.text ?? "Empty post"}</strong>
215 </article>
216 )}
217/>
218```
219
220## Demo
221
222Check out the [live demo](https://atproto-ui.netlify.app/) to see all components in action.
223
224### Running Locally
225
226```bash
227npm install
228npm run dev
229```
230
231## Contributing
232
233Contributions are welcome! Open an issue or PR for:
234- New record type support (e.g., Grain.social photos)
235- Improved documentation
236- Bug fixes or feature requests
237
238## License
239
240MIT