apps/docs/content/sdk-features/environment.mdx
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.
The tlenv object contains values detected at page load. These don't change during a session.
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.
Platform-specific keyboard shortcuts:
import { isAccelKey } from 'tldraw'
// Cmd on Mac, Ctrl elsewhere
if (isAccelKey(e)) {
// Handle the shortcut
}
Mobile detection:
const isMobile = tlenv.isIos || tlenv.isAndroid
if (isMobile) {
// Adjust UI for mobile
}
Browser-specific workarounds:
// Safari needs extra time for SVG image export
if (tlenv.isSafari) {
await new Promise((r) => setTimeout(r, 250))
}
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.
import { tlenvReactive, useValue } from 'tldraw'
function TouchFriendlyButton() {
const { isCoarsePointer } = useValue(tlenvReactive)
return (
<button style={{ padding: isCoarsePointer ? 16 : 8 }}>
Click me
</button>
)
}
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.
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.
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:
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:
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.
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 for isCoarsePointer on the instance record, which mirrors tlenvReactive.