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