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![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`, `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### Using atcute Directly 115 116Use atcute directly to construct records and pass them to components—fully compatible! 117 118```tsx 119import { Client, simpleFetchHandler, ok } from '@atcute/client'; 120import type { AppBskyFeedPost } from '@atcute/bluesky'; 121import { BlueskyPost } from 'atproto-ui'; 122 123// Create atcute client 124const client = new Client({ 125 handler: simpleFetchHandler({ service: 'https://public.api.bsky.app' }) 126}); 127 128// Fetch a record 129const data = await ok( 130 client.get('com.atproto.repo.getRecord', { 131 params: { 132 repo: 'did:plc:ttdrpj45ibqunmfhdsb4zdwq', 133 collection: 'app.bsky.feed.post', 134 rkey: '3m45rq4sjes2h' 135 } 136 }) 137); 138 139const record = data.value as AppBskyFeedPost.Main; 140 141// Pass atcute record directly to component! 142<BlueskyPost 143 did="did:plc:ttdrpj45ibqunmfhdsb4zdwq" 144 rkey="3m45rq4sjes2h" 145 record={record} 146/> 147``` 148 149## API Reference 150 151### Components 152 153| Component | Description | 154|-----------|-------------| 155| `AtProtoProvider` | Context provider for sharing protocol clients. Optional `plcDirectory` prop. | 156| `AtProtoRecord` | Core component for fetching/rendering any AT Protocol record. Accepts `record` prop. | 157| `BlueskyProfile` | Profile card for a DID/handle. Accepts `record`, `fallback`, `loadingIndicator`, `renderer`. | 158| `BlueskyPost` | Single Bluesky post. Accepts `record`, `iconPlacement`, custom renderers. | 159| `BlueskyQuotePost` | Post with quoted post support. Accepts `record`. | 160| `BlueskyPostList` | Paginated list of posts (default: 5 per page). | 161| `TangledString` | Tangled string (code snippet) renderer. Accepts `record`. | 162| `LeafletDocument` | Long-form document with blocks. Accepts `record`, `publicationRecord`. | 163 164### Hooks 165 166| Hook | Returns | 167|------|---------| 168| `useDidResolution(did)` | `{ did, handle, loading, error }` | 169| `useLatestRecord(did, collection)` | `{ record, rkey, loading, error, empty }` | 170| `usePaginatedRecords(options)` | `{ records, loading, hasNext, loadNext, ... }` | 171| `useBlob(did, cid)` | `{ url, loading, error }` | 172| `useAtProtoRecord(did, collection, rkey)` | `{ record, loading, error }` | 173 174## Advanced Usage 175 176### Using Hooks for Custom Logic 177 178```tsx 179import { useLatestRecord, BlueskyPost } from "atproto-ui"; 180import type { FeedPostRecord } from "atproto-ui"; 181 182const LatestBlueskyPost: React.FC<{ did: string }> = ({ did }) => { 183 const { record, rkey, loading, error, empty } = useLatestRecord<FeedPostRecord>( 184 did, 185 "app.bsky.feed.post", 186 ); 187 188 if (loading) return <p>Fetching latest post</p>; 189 if (error) return <p>Could not load: {error.message}</p>; 190 if (empty || !record || !rkey) return <p>No posts yet.</p>; 191 192 // Pass both record and rkey—no additional API call needed 193 return <BlueskyPost did={did} rkey={rkey} record={record} colorScheme="system" />; 194}; 195``` 196 197### Custom Renderer 198 199Use `AtProtoRecord` with a custom renderer for full control: 200 201```tsx 202import { AtProtoRecord } from "atproto-ui"; 203import type { FeedPostRecord } from "atproto-ui"; 204 205<AtProtoRecord<FeedPostRecord> 206 did={did} 207 collection="app.bsky.feed.post" 208 rkey={rkey} 209 renderer={({ record, loading, error }) => ( 210 <article> 211 <strong>{record?.text ?? "Empty post"}</strong> 212 </article> 213 )} 214/> 215``` 216 217## Demo 218 219Check out the [live demo](https://atproto-ui.netlify.app/) to see all components in action. 220 221### Running Locally 222 223```bash 224npm install 225npm run dev 226``` 227 228## Contributing 229 230Contributions are welcome! Open an issue or PR for: 231- New record type support (e.g., Grain.social photos) 232- Improved documentation 233- Bug fixes or feature requests 234 235## License 236 237MIT