snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
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
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
This commit is contained in:
154
apps/docs/content/sdk-features/environment.mdx
Normal file
154
apps/docs/content/sdk-features/environment.mdx
Normal file
@@ -0,0 +1,154 @@
|
||||
---
|
||||
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`.
|
||||
Reference in New Issue
Block a user