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