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
218 lines
7.5 KiB
Plaintext
218 lines
7.5 KiB
Plaintext
---
|
|
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
|