Files
upstream-tldraw/apps/docs/content/sdk-features/environment.mdx
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

155 lines
5.9 KiB
Plaintext

---
title: Environment detection
created_at: 12/22/2025
updated_at: 12/29/2025
keywords:
- environment
- browser
- platform
- tlenv
- Safari
- iOS
- Android
- Firefox
- coarse pointer
reviewed_by: steveruizok
status: published
date: 12/22/2025
readability: 9
voice: 9
completeness: 8
accuracy: 10
notes: ''
---
The tldraw SDK provides two objects for detecting the user's environment. [tlenv](?) holds fixed browser and platform information. [tlenvReactive](?) holds values that change during a session, like whether the user is currently using touch input. We use these internally to work around browser quirks, and you can use them in custom shapes or tools.
## Static environment: tlenv
The `tlenv` object contains values detected at page load. These don't change during a session.
```typescript
import { tlenv } from 'tldraw'
// Browser detection
tlenv.isSafari // true if Safari (excluding Chrome on iOS)
tlenv.isFirefox // true if Firefox
tlenv.isChromeForIos // true if Chrome running on iOS
// Platform detection
tlenv.isIos // true if iPad or iPhone
tlenv.isAndroid // true if Android device
tlenv.isDarwin // true if macOS
// Capability detection
tlenv.hasCanvasSupport // true if Promise and HTMLCanvasElement exist
tlenv.isTouchDevice // true if the device has a touch screen
```
`isTouchDevice` reflects the hardware, so it stays true on a touchscreen laptop even while the user is using a mouse. For the pointer currently in use, read `isCoarsePointer` from `tlenvReactive` below.
### Common patterns
**Platform-specific keyboard shortcuts:**
```typescript
import { isAccelKey } from 'tldraw'
// Cmd on Mac, Ctrl elsewhere
if (isAccelKey(e)) {
// Handle the shortcut
}
```
**Mobile detection:**
```typescript
const isMobile = tlenv.isIos || tlenv.isAndroid
if (isMobile) {
// Adjust UI for mobile
}
```
**Browser-specific workarounds:**
```typescript
// Safari needs extra time for SVG image export
if (tlenv.isSafari) {
await new Promise((r) => setTimeout(r, 250))
}
```
## Reactive environment: tlenvReactive
The `tlenvReactive` atom contains values that can change during a session: `isCoarsePointer` (the current pointer type) and `supportsP3ColorSpace` (whether the current display supports the P3 color gamut). Use [useValue](?) to subscribe to changes in React components.
```tsx
import { tlenvReactive, useValue } from 'tldraw'
function TouchFriendlyButton() {
const { isCoarsePointer } = useValue(tlenvReactive)
return (
<button style={{ padding: isCoarsePointer ? 16 : 8 }}>
{/* Larger touch target when using touch input */}
Click me
</button>
)
}
```
### Coarse pointer detection
The `isCoarsePointer` value tracks whether the user is currently using touch input. We detect this two ways: by listening to the `(any-pointer: coarse)` media query, and by checking `pointerType` on each pointer down event. Any pointer that isn't a mouse counts as coarse, so pen input switches to coarse mode too. Laptops with touchscreens can switch input methods mid-session; the pointer down check catches that.
```typescript
import { tlenvReactive, react } from 'tldraw'
// Access the current value directly
const isCoarse = tlenvReactive.get().isCoarsePointer
// Subscribe to changes outside React
react('pointer type changed', () => {
const { isCoarsePointer } = tlenvReactive.get()
console.log('Coarse pointer:', isCoarsePointer)
})
```
Note: We force fine pointer mode on Firefox desktop regardless of the actual input device, since Firefox's coarse pointer reporting is unreliable there.
## Cross-window contexts
When the editor is mounted inside an iframe, an Electron pop-out window, or an Obsidian plugin, the global `document` and `window` aren't necessarily the ones the editor's DOM lives in. Reading `document.activeElement` or attaching a listener to the global `window` will silently fail or target the wrong frame.
Use the editor's container helpers instead of bare globals. They are marked internal, so their signatures may change, but they are what the SDK itself uses:
```typescript
const doc = editor.getContainerDocument() // the document the editor is mounted in
const win = editor.getContainerWindow() // the window the editor is mounted in
```
When you have a DOM node and want the document or window that owns it (rather than where the editor lives), use the standalone helpers:
```typescript
import { getOwnerDocument, getOwnerWindow } from 'tldraw'
const doc = getOwnerDocument(node)
const win = getOwnerWindow(node)
```
These are what the editor uses internally for export, measurement, focus tracking, and event listeners. Custom shapes and tools that touch the DOM directly should use them too. Anything that calls `document` or `window` directly will target the wrong realm once your app is embedded in an iframe or pop-out window.
## Browser quirks we handle
Here's a sample of what we use environment detection for internally.
**Safari** has the most workarounds. During SVG-to-image export, Safari fires the image's load event before the fonts inside the SVG have loaded, so we wait an extra 250ms—a WebKit bug that's been open for years. Text outlines are a performance problem on Safari, so we disable them there.
**iOS** support for coalesced pointer events is unreliable (`getCoalescedEvents` is sometimes missing entirely), so we skip coalesced events on iOS and dispatch individual pointer events instead. We also skip Safari's proprietary `GestureEvent` on iOS.
**Firefox** desktop's `(any-pointer: coarse)` media query reports false positives when a touchscreen is present but not in use. We force fine pointer mode on Firefox desktop to avoid jumpy UI.
**Chrome for iOS** has its own print implementation that doesn't trigger the standard `beforeprint` event, so we call our print handler manually before printing. Safari desktop needs `document.execCommand('print')` instead of `window.print()`.
See [Instance state](/sdk-features/instance-state) for `isCoarsePointer` on the instance record, which mirrors `tlenvReactive`.