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, check out lib/styles.css */
64```
65
66### Override Component Theme
67
68Wrap any component in a div with custom CSS variables to override its appearance:
69
70```tsx
71import { AtProtoStyles } from "atproto-ui";
72
73<div style={{
74 '--atproto-color-bg': '#f0f0f0',
75 '--atproto-color-text': '#000',
76 '--atproto-color-link': '#0066cc',
77} satisfies AtProtoStyles}>
78 <BlueskyPost did="..." rkey="..." />
79</div>
80```
81
82## Prefetched Data
83
84All 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.
85
86```tsx
87import { BlueskyPost, useLatestRecord } from "atproto-ui";
88import type { FeedPostRecord } from "atproto-ui";
89
90const MyComponent: React.FC<{ did: string }> = ({ did }) => {
91 // Fetch the latest post using the hook
92 const { record, rkey, loading } = useLatestRecord<FeedPostRecord>(
93 did,
94 "app.bsky.feed.post"
95 );
96
97 if (loading) return <p>Loading…</p>;
98 if (!record || !rkey) return <p>No posts found.</p>;
99
100 // Pass the fetched record directly—BlueskyPost won't re-fetch it
101 return <BlueskyPost did={did} rkey={rkey} record={record} />;
102};
103```
104
105All components support prefetched data:
106
107```tsx
108<BlueskyProfile did={did} record={profileRecord} />
109<TangledString did={did} rkey={rkey} record={stringRecord} />
110<LeafletDocument did={did} rkey={rkey} record={documentRecord} />
111```
112
113## API Reference
114
115### Components
116
117| Component | Description |
118|-----------|-------------|
119| `AtProtoProvider` | Context provider for sharing protocol clients. Optional `plcDirectory` prop. |
120| `AtProtoRecord` | Core component for fetching/rendering any AT Protocol record. Accepts `record` prop. |
121| `BlueskyProfile` | Profile card for a DID/handle. Accepts `record`, `fallback`, `loadingIndicator`, `renderer`. |
122| `BlueskyPost` | Single Bluesky post. Accepts `record`, `iconPlacement`, custom renderers. |
123| `BlueskyQuotePost` | Post with quoted post support. Accepts `record`. |
124| `BlueskyPostList` | Paginated list of posts (default: 5 per page). |
125| `TangledString` | Tangled string (code snippet) renderer. Accepts `record`. |
126| `LeafletDocument` | Long-form document with blocks. Accepts `record`, `publicationRecord`. |
127
128### Hooks
129
130| Hook | Returns |
131|------|---------|
132| `useDidResolution(did)` | `{ did, handle, loading, error }` |
133| `useLatestRecord(did, collection)` | `{ record, rkey, loading, error, empty }` |
134| `usePaginatedRecords(options)` | `{ records, loading, hasNext, loadNext, ... }` |
135| `useBlob(did, cid)` | `{ url, loading, error }` |
136| `useAtProtoRecord(did, collection, rkey)` | `{ record, loading, error }` |
137
138## Advanced Usage
139
140### Using Hooks for Custom Logic
141
142```tsx
143import { useLatestRecord, BlueskyPost } from "atproto-ui";
144import type { FeedPostRecord } from "atproto-ui";
145
146const LatestBlueskyPost: React.FC<{ did: string }> = ({ did }) => {
147 const { record, rkey, loading, error, empty } = useLatestRecord<FeedPostRecord>(
148 did,
149 "app.bsky.feed.post",
150 );
151
152 if (loading) return <p>Fetching latest post…</p>;
153 if (error) return <p>Could not load: {error.message}</p>;
154 if (empty || !record || !rkey) return <p>No posts yet.</p>;
155
156 // Pass both record and rkey—no additional API call needed
157 return <BlueskyPost did={did} rkey={rkey} record={record} colorScheme="system" />;
158};
159```
160
161### Custom Renderer
162
163Use `AtProtoRecord` with a custom renderer for full control:
164
165```tsx
166import { AtProtoRecord } from "atproto-ui";
167import type { FeedPostRecord } from "atproto-ui";
168
169<AtProtoRecord<FeedPostRecord>
170 did={did}
171 collection="app.bsky.feed.post"
172 rkey={rkey}
173 renderer={({ record, loading, error }) => (
174 <article>
175 <strong>{record?.text ?? "Empty post"}</strong>
176 </article>
177 )}
178/>
179```
180
181## Demo
182
183Check out the [live demo](https://atproto-ui.wisp.place/) to see all components in action.
184
185### Running Locally
186
187```bash
188npm install
189npm run dev
190```
191
192## Contributing
193
194Contributions are welcome! Open an issue or PR for:
195- New record type support (e.g., Grain.social photos)
196- Improved documentation
197- Bug fixes or feature requests
198
199## License
200
201MIT