Files
upstream-mirror a774ac673a
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
snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
2026-09-23 16:12:52 +05:00

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