A React component library for rendering common AT Protocol records for applications such as Bluesky and Leaflet.
1# atproto-ui
2
3atproto-ui is a component library and set of hooks for rendering records from the AT Protocol (Bluesky, Leaflet, and friends) in React applications. It handles DID resolution, PDS endpoint discovery, and record fetching so you can focus on UI. [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`, etc.).
13- Hooks and helpers for composing your own renderers for your own applications, (PRs welcome!)
14- Built on the lightweight [`@atcute/*`](https://github.com/atcute) clients.
15
16## Installation
17
18```bash
19npm install atproto-ui
20```
21
22## Quick start
23
241. Wrap your app (once) with the `AtProtoProvider`.
252. Drop any of the ready-made components inside that provider.
263. Use the hooks to prefetch handles, blobs, or latest records when you want to control the render flow yourself.
27
28```tsx
29import { AtProtoProvider, BlueskyPost } from "atproto-ui";
30
31export function App() {
32 return (
33 <AtProtoProvider>
34 <BlueskyPost did="did:plc:example" rkey="3k2aexample" />
35 {/* you can use handles in the components as well. */}
36 <LeafletDocument did="nekomimi.pet" rkey="3m2seagm2222c" />
37 </AtProtoProvider>
38 );
39}
40```
41
42### Available building blocks
43
44| Component / Hook | What it does |
45| --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
46| `AtProtoProvider` | Configures PLC directory (defaults to `https://plc.directory`) and shares protocol clients via React context. |
47| `BlueskyProfile` | Renders a profile card for a DID/handle. Accepts `fallback`, `loadingIndicator`, `renderer`, and `colorScheme`. |
48| `BlueskyPost` / `BlueskyQuotePost` | Shows a single Bluesky post, with quotation support, custom renderer overrides, and the same loading/fallback knobs. |
49| `BlueskyPostList` | Lists the latest posts with built-in pagination (defaults: 5 per page, pagination controls on). |
50| `TangledString` | Renders a Tangled string (gist-like record) with optional renderer overrides. |
51| `LeafletDocument` | Displays long-form Leaflet documents with blocks, theme support, and renderer overrides. |
52| `useDidResolution`, `useLatestRecord`, `usePaginatedRecords`, … | Hook-level access to records if you want to own the markup or prefill components. |
53
54All components accept a `colorScheme` of `'light' | 'dark' | 'system'` so they can blend into your design. They also accept `fallback` and `loadingIndicator` props to control what renders before or during network work, and most expose a `renderer` override when you need total control of the final markup.
55
56### Prefill components with the latest record
57
58`useLatestRecord` gives you the most recent record for any collection along with its `rkey`. You can use that key to pre-populate components like `BlueskyPost`, `LeafletDocument`, or `TangledString`.
59
60```tsx
61import { useLatestRecord, BlueskyPost } from "atproto-ui";
62import type { FeedPostRecord } from "atproto-ui";
63
64const LatestBlueskyPost: React.FC<{ did: string }> = ({ did }) => {
65 const { rkey, loading, error, empty } = useLatestRecord<FeedPostRecord>(
66 did,
67 "app.bsky.feed.post",
68 );
69
70 if (loading) return <p>Fetching latest post…</p>;
71 if (error) return <p>Could not load: {error.message}</p>;
72 if (empty || !rkey) return <p>No posts yet.</p>;
73
74 return <BlueskyPost did={did} rkey={rkey} colorScheme="system" />;
75};
76```
77
78The same pattern works for other components: swap the collection NSID and the component you render once you have an `rkey`.
79
80```tsx
81const LatestLeafletDocument: React.FC<{ did: string }> = ({ did }) => {
82 const { rkey } = useLatestRecord(did, "pub.leaflet.document");
83 return rkey ? (
84 <LeafletDocument did={did} rkey={rkey} colorScheme="light" />
85 ) : null;
86};
87```
88
89## Compose your own component
90
91The helpers let you stitch together custom experiences without reimplementing protocol plumbing. The example below pulls a creator’s latest post and renders a minimal summary:
92
93```tsx
94import { useLatestRecord, useColorScheme, AtProtoRecord } from "atproto-ui";
95import type { FeedPostRecord } from "atproto-ui";
96
97const LatestPostSummary: React.FC<{ did: string }> = ({ did }) => {
98 const scheme = useColorScheme("system");
99 const { rkey, loading, error } = useLatestRecord<FeedPostRecord>(
100 did,
101 "app.bsky.feed.post",
102 );
103
104 if (loading) return <span>Loading…</span>;
105 if (error || !rkey) return <span>No post yet.</span>;
106
107 return (
108 <AtProtoRecord<FeedPostRecord>
109 did={did}
110 collection="app.bsky.feed.post"
111 rkey={rkey}
112 renderer={({ record }) => (
113 <article data-color-scheme={scheme}>
114 <strong>{record?.text ?? "Empty post"}</strong>
115 </article>
116 )}
117 />
118 );
119};
120```
121
122There is a [demo](https://react-ui.wisp.place) where you can see the components in live action.
123
124## Running the demo locally
125
126```bash
127npm install
128npm run dev
129```
130
131Then open the printed Vite URL and try entering a Bluesky handle to see the components in action.
132
133## Next steps
134
135- Expand renderer coverage (e.g., Grain.social photos).
136- Expand documentation with TypeScript API references and theming guidelines.
137
138Contributions and ideas are welcome—feel free to open an issue or PR!