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