Troubleshooting
Common Issues
Blank screen / no output
- Check that
using term = createTerm()is called beforerender(). - Ensure stdout is a TTY. Piped output needs
renderString()instead ofrender(). - Verify the component returns visible content (not
nullor empty fragments).
Content overflows / truncation
- Text auto-truncates by default. Use
wrap="wrap"on Text to wrap instead, orwrap="overflow"to allow overflow. - Check
overflow="hidden"on the parent Box to clip children. - For scrollable content, use
overflow="scroll"with ascrollToprop.
Incremental rendering mismatches
- Run with
SILVERY_STRICT=1to compare incremental vs fresh render output on every frame. - If a mismatch is found, file a bug with the minimal reproduction.
- Use
withDiagnostics({ checkIncremental: true })in tests to catch these automatically.
useInput not receiving keys
- Ensure
InputLayerProviderwraps your app if usinguseInputLayer. - Check that stdin is in raw mode (
term.caps.inputreturnstrue). - Verify no other input layer is consuming the key before yours (layers are LIFO).
Layout oscillation / infinite loops
- Silvery has built-in containment for
useBoxRectthat detects and breaks oscillation loops. - If you see oscillation, check for circular dependencies in layout — e.g., a component that changes its size based on
useBoxRectin a way that triggers another layout. - Avoid setting
widthorheightdynamically based onuseBoxRectof the same Box.
Flickering in tmux / Zellij
- Silvery uses Synchronized Update Mode (DEC 2026) by default, which prevents flicker. Verify your multiplexer version supports it (tmux 3.2+).
- To disable sync updates for debugging:
SILVERY_SYNC_UPDATE=0.
Colors not appearing
- Check
term.caps.colorLevel— returns"mono"ifNO_COLORis set orTERM=dumb. - Verify
COLORTERM=truecoloris set for true color support. - Some CI environments strip ANSI codes. Use
renderString(<App />, { plain: true })for those.
Kitty keyboard shortcuts not working
run()auto-enables the Kitty protocol on supported terminals. If usingcreateApp().run(), passkitty: trueexplicitly.- Check that your terminal supports it (Ghostty, Kitty, WezTerm, foot — see Terminal Capabilities).
- iTerm2 and Terminal.app do not support the Kitty protocol.
Flexily vs Yoga layout differences
Some layout behaviors differ between Flexily and Yoga (the engine Ink uses), so if you're mixing the two or copying examples, watch for:
- Percentage widths: Flexily resolves
width="50%"against the parent's content area. Yoga resolves against the parent's total width including padding. If your layout is off by a few cells, check padding on the parent. - Default
flexShrink: Both default to 1, but Flexily may clamp earlier on zero-width children. If a child collapses unexpectedly, setflexShrink={0}explicitly. flexBasis="auto": Yoga uses the intrinsic content size. Flexily does the same but measures text differently for multi-line content. If text wraps unexpectedly, set an explicitwidth.- Gap: Flexily supports
gap,rowGap,columnGapthe same as Yoga. If gaps don't appear, verifyflexDirectionis set (gap only applies between flex children along the main axis).
Switch engines to isolate: SILVERY_ENGINE=yoga bun run app.ts
Focus routing debugging
If focus isn't moving where expected:
- Enable inspector:
SILVERY_DEV=1to visualize the focus tree and see which nodes arefocusable. - Check
focusableprop: Only<Box focusable>nodes participate in focus. Text nodes cannot receive focus. - Verify
testID: Spatial navigation overrides (nextFocusUp,nextFocusDown, etc.) reference nodes bytestID. Missing or mismatched IDs are silently ignored. - Scope boundaries: If Tab doesn't leave a subtree, a parent has
focusScopewhich restricts cycling to that subtree. Remove or restructure the scope. - Debug with
FocusManager: Accessfm.activeIdandfm.focusOriginto see what's focused and why.
VirtualList blank rendering
VirtualList shows blank rows when:
estimateHeightis far off: ListView/VirtualList measures actual rendered heights after layout and uses them for scroll math. However, a very inaccurateestimateHeightcan cause jumpiness on the first render before measurements stabilize. Set it to the most common item height.dataarray changes identity on every render: Wrap withuseMemoor hoist the array. Identity changes cause VirtualList to recalculate offsets.- Container has no height: VirtualList needs a parent with a known height (explicit
heightprop or flex layout). Without it, the visible window is zero and nothing renders. scrollTois out of range: AscrollTovalue past the last item shows blank space. Clamp toMath.max(0, data.length - visibleCount).
useBoxRect returning zeros
useBoxRect() returns { width: 0, height: 0 } on the first render because layout hasn't run yet. This is by design.
- Normal case: The component re-renders once layout completes (usually the same frame). Your UI should handle
width === 0gracefully (returnnullor a placeholder). - Not updating at all: Ensure the component's parent has a concrete size. A chain of
flexGrow={1}without a root-level size means layout can't resolve. - In tests: Call
app.debug()after render — if the layout is correct in the debug output, the hook is working. Ifwidthstays 0, check thatcreateRendererhascolsandrowsset. - Oscillation: If the component changes its own size based on
useBoxRect, it can loop. Silvery's built-in containment detects this and breaks the cycle automatically.
Debugging
Runtime Debug
See debugging.md for the full reference: STRICT verification modes, diagnostic workflow, and symptom→check cross-reference.
bash
SILVERY_STRICT=1 bun run app.ts # Buffer-level (includes vt100 output check)
SILVERY_STRICT_TERMINAL=xterm bun run app.ts # Independent terminal verification
DEBUG=silvery:* DEBUG_LOG=/tmp/silvery.log bun run app.ts # Pipeline tracesTest Debug
tsx
const app = render(<MyComponent />)
app.debug() // Print current frame to console
console.log(app.ansi) // Print with colors
console.log(app.text) // Print plain text (no ANSI)