Custom user metadata
Store app-specific fields like roles and departments in TLUser.meta.
import {
atom,
computed,
createCachedUserResolve,
createUserId,
Tldraw,
TldrawUiButton,
TLUser,
TLUserStore,
useEditor,
UserRecordType,
useValue,
} from 'tldraw'
import 'tldraw/tldraw.css'
import './custom-user.css'
// There's a guide at the bottom of this file!
// [1]
interface CustomUserMeta {
isAdmin: boolean
department: string
}
function asCustomMeta(user: TLUser | null): CustomUserMeta | null {
if (!user) return null
return user.meta as unknown as CustomUserMeta
}
// [2]
const usersAtom = atom<Record<string, TLUser>>('users', {
[createUserId('alice')]: UserRecordType.create({
id: createUserId('alice'),
name: 'Alice',
color: '#e03131',
meta: { isAdmin: true, department: 'Engineering' },
}),
[createUserId('bob')]: UserRecordType.create({
id: createUserId('bob'),
name: 'Bob',
color: '#1971c2',
meta: { isAdmin: false, department: 'Design' },
}),
[createUserId('carol')]: UserRecordType.create({
id: createUserId('carol'),
name: 'Carol',
color: '#2f9e44',
meta: { isAdmin: false, department: 'Product' },
}),
})
const currentUserIdAtom = atom('currentUserId', createUserId('alice'))
// [3]
const currentUserSignal = computed('currentUser', () => {
return usersAtom.get()[currentUserIdAtom.get()] ?? null
})
const users: TLUserStore = {
currentUser: currentUserSignal,
resolve: createCachedUserResolve((userId) => usersAtom.get()[createUserId(userId)] ?? null),
}
// [4]
function UserSwitcher() {
const allUsers = useValue(usersAtom)
const activeUserId = useValue(currentUserIdAtom)
return (
<div className="tlui-menu custom-user-controls">
{Object.values(allUsers).map((user) => (
<TldrawUiButton
key={user.id}
type={activeUserId === user.id ? 'primary' : 'normal'}
onClick={() => currentUserIdAtom.set(user.id)}
>
<span className="custom-user-dot" style={{ backgroundColor: user.color }} />
{user.name}
</TldrawUiButton>
))}
</div>
)
}
// [5]
function CustomUserPanel() {
const editor = useEditor()
const currentUser = useValue(editor.store.props.users.currentUser)
const customMeta = asCustomMeta(currentUser)
return (
<div className="custom-user-panel">
{currentUser && customMeta ? (
<>
<div className="custom-user-header">
<span className="custom-user-dot-lg" style={{ backgroundColor: currentUser.color }} />
<span className="custom-user-name">{currentUser.name}</span>
{customMeta.isAdmin && <span className="custom-user-badge">Admin</span>}
</div>
<div className="custom-user-row">
<span className="custom-user-label">Department</span>
<span>{customMeta.department}</span>
</div>
<div className="custom-user-row">
<span className="custom-user-label">Role</span>
<span>{customMeta.isAdmin ? 'Administrator' : 'Member'}</span>
</div>
</>
) : (
<div className="custom-user-hint">No user selected</div>
)}
</div>
)
}
const components = {
TopPanel: UserSwitcher,
SharePanel: CustomUserPanel,
}
// [6]
export default function CustomUserExample() {
return (
<div className="tldraw__editor">
<Tldraw persistenceKey="custom-user-example" users={users} components={components} />
</div>
)
}
/*
[1]
`TLUser.meta` is typed as `JsonObject`, so it accepts any JSON-compatible data. An
interface plus a cast on read gives you typed access. For runtime validation, pass
validators to `createTLSchema` (see the README).
[2]
A fake user directory in a reactive atom, with `isAdmin` and `department` in each
user's `meta`. In a real app this would come from your auth system.
[3]
The `TLUserStore`. `currentUser` and `resolve` are signals derived from the atoms, so
anything reading them re-evaluates when the data changes.
[4]
Switch which user is "logged in".
[5]
The panel reads the current user signal and shows both the standard fields (name,
color) and the custom meta (department, admin badge).
[6]
Pass the user store via the `users` prop. Custom metadata rides along on the same
`TLUser` records the editor already manages, so nothing else needs wiring.
*/
TLUser records have a meta field for your own data. This example gives each user an isAdmin flag and a department, provides them through a TLUserStore, and reads them back in a side panel. Switch users with the buttons at the top to see the panel update.
meta is untyped JSON, so the example casts to a local interface when reading. For runtime validation, pass validators to createTLSchema and hand the resulting schema to your store:
const schema = createTLSchema({
user: {
meta: {
isAdmin: T.boolean,
department: T.string,
},
},
})Is this page helpful?
Next
Multiplayer sync with custom user data