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