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![Bluesky component](readme_img/bluesky.png) 8![Tangled String component](readme_img/tangled.png) 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!