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
83 lines
4.0 KiB
Plaintext
83 lines
4.0 KiB
Plaintext
---
|
|
title: Pen mode
|
|
created_at: 1/31/2026
|
|
updated_at: 1/31/2026
|
|
keywords:
|
|
- pen mode
|
|
- stylus
|
|
- pressure sensitivity
|
|
- touch rejection
|
|
- palm rejection
|
|
status: draft
|
|
date: 1/31/2026
|
|
---
|
|
|
|
Pen mode restricts canvas input to stylus events. When a stylus touches a touch-capable screen, like an Apple Pencil on an iPad, the editor turns on pen mode and ignores other pointer input. Users can rest their palm on the screen while drawing without triggering accidental touches.
|
|
|
|
```tsx
|
|
import { Tldraw } from 'tldraw'
|
|
import 'tldraw/tldraw.css'
|
|
|
|
export default function App() {
|
|
return (
|
|
<div style={{ position: 'fixed', inset: 0 }}>
|
|
<Tldraw
|
|
onMount={(editor) => {
|
|
if (editor.getInstanceState().isPenMode) {
|
|
editor.updateInstanceState({ isPenMode: false })
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
## How pen mode activates
|
|
|
|
The editor detects stylus input from the browser's pointer events: any pointer with `pointerType: 'pen'` is treated as a pen. Pen mode only turns on for direct-display pens, though. When a pen touches the canvas on a touch-capable device, the editor sets `isPenMode` to `true` on the [instance state](/sdk-features/instance-state) and interrupts any touch interaction in progress. A stylus on a desktop graphics tablet still draws as a pen but doesn't enable pen mode, since there's no palm to reject.
|
|
|
|
Pen mode is session state. It doesn't persist across reloads, and it isn't synced between collaborators. There's no option to disable auto-enabling; if you don't want it, turn it back off with [Editor#updateInstanceState](?).
|
|
|
|
## Touch rejection
|
|
|
|
While pen mode is on, the editor drops pointer down, move, and up events from any non-pen input. Finger touches and mouse clicks on the canvas do nothing. This is what makes palm rejection work: a hand resting on a tablet screen produces touch events, and pen mode filters them all out.
|
|
|
|
The rest of the UI still works normally. Pen mode only filters events on the canvas, so users can still tap toolbar buttons and menus with a finger. Click detection follows the same rule: in pen mode only pen events produce double-clicks.
|
|
|
|
## Exiting pen mode
|
|
|
|
While pen mode is active, the default UI shows an "Exit pen mode" helper button over the canvas. Clicking it runs the `exit-pen-mode` action, which sets `isPenMode` back to `false`.
|
|
|
|
You can do the same from code:
|
|
|
|
```tsx
|
|
editor.updateInstanceState({ isPenMode: false })
|
|
```
|
|
|
|
There's no way to turn pen mode off with a finger on the canvas itself, since that input is rejected. If your custom UI replaces the default one, keep the helper button or an equivalent control available.
|
|
|
|
## Pressure sensitivity
|
|
|
|
Pen pressure arrives on pointer events as the `z` component of the pointer's position, taken from the browser's `PointerEvent.pressure` value (0 to 1).
|
|
|
|
The draw tool uses this pressure to vary stroke thickness. When the input reports real pressure (a `pen` pointer with non-zero pressure, or any pointer whose pressure isn't a constant 0, 0.5, or 1), the resulting draw shape records `isPen: true` in its props and stores scaled pressure in its points. Strokes drawn with a mouse or finger use simulated pressure instead. The highlighter records `isPen` the same way but renders at constant width.
|
|
|
|
```tsx
|
|
// Read the current pointer's pressure
|
|
const { z } = editor.inputs.getCurrentPagePoint()
|
|
|
|
// Check whether the most recent pointer event came from a pen
|
|
const isPen = editor.inputs.getIsPen()
|
|
```
|
|
|
|
## Stylus erasers
|
|
|
|
Some styluses, like the Surface Pen or Wacom pens, have a hardware eraser. When the editor receives a pointer down from an eraser button (button 5), it switches to the eraser tool, then restores the previous tool when the eraser lifts. This works whether or not pen mode is on.
|
|
|
|
## Related articles
|
|
|
|
- [Instance state](/sdk-features/instance-state) — Where `isPenMode` lives, along with other session state
|
|
- [Input handling](/sdk-features/input-handling) — How the editor processes pointer events
|
|
- [Draw shape](/sdk-features/draw-shape) — The freehand shape that records pen pressure
|