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