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
144 lines
5.4 KiB
Plaintext
144 lines
5.4 KiB
Plaintext
---
|
|
title: Visibility
|
|
status: published
|
|
author: steveruizok
|
|
date: 1/31/2026
|
|
keywords:
|
|
- culling
|
|
- visibility
|
|
- rendering
|
|
- performance
|
|
- opacity
|
|
- hidden
|
|
- viewport
|
|
---
|
|
|
|
The editor's visibility system determines which shapes are rendered on screen. It handles two separate concerns: culling, which hides off-screen shapes for performance, and hidden shapes, which your application hides explicitly.
|
|
|
|
## Culling
|
|
|
|
Culling is a performance optimization. Shapes outside the viewport stay in the store and can still be selected, updated, or queried, but the editor sets `display: none` on their DOM elements. Selected shapes and the shape being edited are never culled. Return `false` from [ShapeUtil#canCull](?) to opt a shape out.
|
|
|
|
You can get the set of culled shape IDs with [Editor#getCulledShapes](?):
|
|
|
|
```ts
|
|
const culledIds = editor.getCulledShapes()
|
|
```
|
|
|
|
See [Culling](/sdk-features/culling) for how the spatial index decides what to cull and for the full set of culling methods.
|
|
|
|
## Hidden shapes
|
|
|
|
Hidden shapes are shapes your application explicitly hides using the `getShapeVisibility` option. Unlike culled shapes, hidden shapes are excluded from hit tests, exports, and rendering.
|
|
|
|
```tsx
|
|
import { Tldraw } from 'tldraw'
|
|
import 'tldraw/tldraw.css'
|
|
|
|
export default function App() {
|
|
return (
|
|
<div style={{ position: 'fixed', inset: 0 }}>
|
|
<Tldraw
|
|
getShapeVisibility={(shape) => {
|
|
if (shape.meta.hidden) return 'hidden'
|
|
return 'inherit'
|
|
}}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
The function can return:
|
|
|
|
| Return value | Behavior |
|
|
| ----------------------------------- | ------------------------------------------------------- |
|
|
| `'inherit'`, `undefined`, or `null` | Shape is visible unless its parent is hidden (default). |
|
|
| `'hidden'` | Shape is always hidden. |
|
|
| `'visible'` | Shape is always visible, even if parent is hidden. |
|
|
|
|
The same prop is available on [TldrawEditor](?) and as an [Editor](?) constructor option. The editor caches the result per shape, so keep the callback pure.
|
|
|
|
Hidden shapes are still in the store. They're just excluded from:
|
|
|
|
- Canvas rendering
|
|
- Hit tests ([Editor#getShapeAtPoint](?), [Editor#getShapesAtPoint](?))
|
|
- [Editor#getRenderingShapes](?) and [Editor#getCurrentPageRenderingShapesSorted](?)
|
|
- Image exports and printing
|
|
- [Editor#getCurrentPageBounds](?) and [Editor#zoomToFit](?)
|
|
- Drop targets and automatic parenting of new shapes
|
|
|
|
You can check if a shape is hidden with [Editor#isShapeHidden](?):
|
|
|
|
```ts
|
|
if (editor.isShapeHidden(shapeId)) {
|
|
// shape won't render
|
|
}
|
|
```
|
|
|
|
### Preventing hidden shapes from being selected
|
|
|
|
Hidden shapes can still be selected via keyboard shortcuts like select-all. If you want to prevent this, filter the [selection](/sdk-features/selection) when it changes:
|
|
|
|
```tsx
|
|
import { react, Tldraw } from 'tldraw'
|
|
import 'tldraw/tldraw.css'
|
|
|
|
export default function App() {
|
|
return (
|
|
<div style={{ position: 'fixed', inset: 0 }}>
|
|
<Tldraw
|
|
getShapeVisibility={(shape) => (shape.meta.hidden ? 'hidden' : 'inherit')}
|
|
onMount={(editor) => {
|
|
return react('filter hidden from selection', () => {
|
|
const selectedIds = editor.getSelectedShapeIds()
|
|
const visibleIds = selectedIds.filter((id) => !editor.isShapeHidden(id))
|
|
if (selectedIds.length !== visibleIds.length) {
|
|
editor.setSelectedShapes(visibleIds)
|
|
}
|
|
})
|
|
}}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
For examples using hidden shapes, see the [collaboration private content example](/examples/collaboration/sync-private-content) and the [layer panel example](/examples/ui/layer-panel).
|
|
|
|
## Opacity
|
|
|
|
Each shape has an `opacity` property (0 to 1) that controls its transparency. When shapes are nested, opacity multiplies down the hierarchy: a shape at 0.5 opacity inside a parent at 0.5 opacity renders at 0.25 opacity.
|
|
|
|
The editor calculates the cumulative opacity for each shape and passes it to the rendering layer. You can access the computed opacity through [Editor#getRenderingShapes](?):
|
|
|
|
```ts
|
|
const renderingShapes = editor.getRenderingShapes()
|
|
for (const { shape, opacity } of renderingShapes) {
|
|
console.log(shape.id, opacity) // cumulative opacity
|
|
}
|
|
```
|
|
|
|
### Erasing feedback
|
|
|
|
When shapes are being erased (the user is mid-gesture with the eraser tool), they render at 32% of their normal opacity. This gives visual feedback that the shapes will be deleted when the gesture completes. The editor handles this automatically—you don't need to implement anything for erasing feedback to work.
|
|
|
|
## Rendering shapes
|
|
|
|
[Editor#getRenderingShapes](?) returns the shapes that should be rendered, along with their computed properties:
|
|
|
|
```ts
|
|
interface TLRenderingShape {
|
|
id: TLShapeId
|
|
shape: TLShape
|
|
util: ShapeUtil
|
|
index: number // z-index for the shape layer
|
|
backgroundIndex: number // z-index for the background layer
|
|
opacity: number // cumulative opacity
|
|
}
|
|
```
|
|
|
|
The `index` and `backgroundIndex` values control z-ordering. The editor uses CSS z-index rather than DOM ordering to position shapes visually. This keeps the DOM stable (shapes stay in ID order) and avoids expensive reflows when z-order changes.
|
|
|
|
For most use cases, you won't need to work with rendering shapes directly; the editor's canvas handles this. Use it if you're building custom rendering or need to know which shapes are visible at what opacity.
|