--- 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 ( ) } ``` ### 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`.