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