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