snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
This commit is contained in:
217
apps/docs/content/sdk-features/attribution.mdx
Normal file
217
apps/docs/content/sdk-features/attribution.mdx
Normal file
@@ -0,0 +1,217 @@
|
||||
---
|
||||
title: Attribution
|
||||
created_at: 3/30/2026
|
||||
updated_at: 3/30/2026
|
||||
keywords:
|
||||
- attribution
|
||||
- user
|
||||
- users
|
||||
- TLUserStore
|
||||
- TLUser
|
||||
- getAttributionUserId
|
||||
- getAttributionDisplayName
|
||||
- getAttributionUser
|
||||
- getReferencedUserIds
|
||||
- createCachedUserResolve
|
||||
- textLastEditedBy
|
||||
- UserRecordType
|
||||
- createUserId
|
||||
status: published
|
||||
date: 3/30/2026
|
||||
---
|
||||
|
||||
The attribution system lets you track which users create or edit shapes. Connect tldraw to your auth system through a [TLUserStore](?) to resolve display names, render attribution labels, and persist user records alongside your document data. The built-in note shape uses attribution to show who last edited a note's text, and you can add similar tracking to custom shapes.
|
||||
|
||||
## User store
|
||||
|
||||
A `TLUserStore` provides a reactive `currentUser` signal for the active user and an optional `resolve` method for looking up other users by ID. Pass it as the `users` prop on the [Tldraw](?) component or the [useSync](?) hook:
|
||||
|
||||
```tsx
|
||||
import { computed, createUserId, Tldraw, TLUserStore, UserRecordType } from 'tldraw'
|
||||
import 'tldraw/tldraw.css'
|
||||
|
||||
const currentUser = computed('currentUser', () =>
|
||||
UserRecordType.create({
|
||||
id: createUserId('user-123'),
|
||||
name: 'Alice',
|
||||
color: '#e03131',
|
||||
})
|
||||
)
|
||||
|
||||
const users: TLUserStore = {
|
||||
currentUser,
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<div style={{ position: 'fixed', inset: 0 }}>
|
||||
<Tldraw users={users} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
When no `users` prop is provided, the editor derives the current user from [user preferences](/sdk-features/user-preferences). With `useSync`, other users are resolved from collaborator presence.
|
||||
|
||||
### Resolving other users
|
||||
|
||||
The optional `resolve` method looks up users by their raw ID string. The editor calls it first whenever it needs a display name or user record for an ID:
|
||||
|
||||
```tsx
|
||||
import {
|
||||
computed,
|
||||
createCachedUserResolve,
|
||||
createUserId,
|
||||
TLUserStore,
|
||||
UserRecordType,
|
||||
} from 'tldraw'
|
||||
|
||||
const users: TLUserStore = {
|
||||
currentUser: computed('currentUser', () =>
|
||||
UserRecordType.create({
|
||||
id: createUserId('user-123'),
|
||||
name: 'Alice',
|
||||
color: '#e03131',
|
||||
})
|
||||
),
|
||||
resolve: createCachedUserResolve((userId) => {
|
||||
// Look up the user from your auth system
|
||||
return myUserCache.get(userId) ?? null
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
The [createCachedUserResolve](?) helper wraps a lookup function so that each user ID gets a single stable reactive signal. If you write your own `resolve`, return the same signal for repeated calls with the same ID.
|
||||
|
||||
## How attribution works
|
||||
|
||||
Attribution is opt-in per shape type. Each shape util decides what to track and when to stamp a user ID. When you stamp a user ID with [Editor#getAttributionUserId](?), the editor also writes a corresponding `user:` record to the store so that display names survive across sessions, clipboard paste, and `.tldr` file exports.
|
||||
|
||||
### Note shape attribution
|
||||
|
||||
The built-in note shape tracks who last edited its text. Whenever a note's rich text changes and is non-empty, [NoteShapeUtil](?) sets the `textLastEditedBy` prop to the current user's ID via `editor.getAttributionUserId()`. Clearing the text resets it to `null`, and duplicating or pasting a note with text re-stamps the copy to the current user. The note renders the last editor's first name as a small label in the corner.
|
||||
|
||||
The label is rendered by the note shape util's `AttributionComponent` option, which you can override or hide with `NoteShapeUtil.configure`, passed through the `shapeUtils` prop. This applies on the canvas and in image exports:
|
||||
|
||||
```tsx
|
||||
import { NoteShapeUtil, Tldraw } from 'tldraw'
|
||||
|
||||
// Hide the attribution badge
|
||||
;<Tldraw shapeUtils={[NoteShapeUtil.configure({ AttributionComponent: null })]} />
|
||||
|
||||
// Or render your own
|
||||
;<Tldraw
|
||||
shapeUtils={[
|
||||
NoteShapeUtil.configure({
|
||||
AttributionComponent: ({ firstName, color }) => (
|
||||
<div className="tl-note__attribution" style={{ color }}>
|
||||
✍️ {firstName}
|
||||
</div>
|
||||
),
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
## Reading attribution
|
||||
|
||||
The [Editor](?) provides three methods for working with attribution. [Editor#getAttributionUserId](?) returns the current user's raw ID string (without the `user:` prefix). [Editor#getAttributionDisplayName](?) and [Editor#getAttributionUser](?) check the `TLUserStore` first, then fall back to `user:` records in the store:
|
||||
|
||||
```tsx
|
||||
import { useEditor, useValue } from 'tldraw'
|
||||
|
||||
function AttributionLabel({ userId }: { userId: string }) {
|
||||
const editor = useEditor()
|
||||
|
||||
const name = useValue('attribution-name', () => editor.getAttributionDisplayName(userId), [
|
||||
editor,
|
||||
userId,
|
||||
])
|
||||
|
||||
if (!name) return null
|
||||
return <span>{name}</span>
|
||||
}
|
||||
```
|
||||
|
||||
## Custom shape attribution
|
||||
|
||||
Add attribution tracking to your own shapes by storing a user ID in your shape's props and overriding [ShapeUtil#getReferencedUserIds](?):
|
||||
|
||||
```tsx
|
||||
import { ShapeUtil, T, TLBaseShape } from 'tldraw'
|
||||
|
||||
type MyShape = TLBaseShape<
|
||||
'my-shape',
|
||||
{
|
||||
createdBy: string | null
|
||||
}
|
||||
>
|
||||
|
||||
class MyShapeUtil extends ShapeUtil<MyShape> {
|
||||
static override type = 'my-shape' as const
|
||||
static override props = {
|
||||
createdBy: T.string.nullable(),
|
||||
}
|
||||
|
||||
override getReferencedUserIds(shape: MyShape) {
|
||||
return shape.props.createdBy ? [shape.props.createdBy] : []
|
||||
}
|
||||
|
||||
// ... other ShapeUtil methods
|
||||
}
|
||||
```
|
||||
|
||||
When shapes are copied to the clipboard or exported, the editor includes the `user:` records returned by `getReferencedUserIds`. Display names then remain available on the other side.
|
||||
|
||||
To stamp the current user when creating shapes:
|
||||
|
||||
```tsx
|
||||
const userId = editor.getAttributionUserId()
|
||||
|
||||
editor.createShape({
|
||||
type: 'my-shape',
|
||||
props: {
|
||||
createdBy: userId,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Extensible user records
|
||||
|
||||
Extend user records with custom metadata by passing validators to [createTLSchema](?):
|
||||
|
||||
```tsx
|
||||
import { createTLSchema, T } from 'tldraw'
|
||||
|
||||
const schema = createTLSchema({
|
||||
user: {
|
||||
meta: {
|
||||
department: T.string,
|
||||
isAdmin: T.boolean,
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Custom metadata is validated and persisted alongside the standard user fields. Access it through the `meta` property on [TLUser](?) records.
|
||||
|
||||
## API reference
|
||||
|
||||
| Symbol | Description |
|
||||
| ------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| [Editor#getAttributionUserId](?) | Get the current user's ID for stamping shapes. Returns `string \| null` |
|
||||
| [Editor#getAttributionDisplayName](?) | Resolve a display name from a user ID. Returns `string \| null` |
|
||||
| [Editor#getAttributionUser](?) | Resolve a full [TLUser](?) record from a user ID |
|
||||
| [TLUserStore](?) | Interface for connecting to your auth/user system |
|
||||
| [TLUser](?) | A user record in the store |
|
||||
| [UserRecordType](?) | The default user record type |
|
||||
| [createUserId](?) | Create a typed user ID |
|
||||
| [createCachedUserResolve](?) | Create a cached resolve function for `TLUserStore` |
|
||||
| [createUserRecordType](?) | Build a user record type with custom meta validators |
|
||||
|
||||
For setting up user identity in multiplayer, see the [Collaboration](/sdk-features/collaboration#User-identity) page.
|
||||
|
||||
## Related examples
|
||||
|
||||
- [Attribution](/examples/users/attribution): basic attribution with a user switcher and attribution inspector
|
||||
- [Attribution timeline](/examples/users/attribution-timeline): timeline scrubber with per-user filtering
|
||||
Reference in New Issue
Block a user