Flex-grid types

Types shared by several @roku-sdk/flex-grid exports, or used by none of them directly

import type {
    CardLayoutContext,
    CardPool,
    CellRenderProps,
    CellVAlign,
    FlexGridClippingConfig,
    FlexGridConfig,
    FlexGridConfigCore,
    FlexGridHandle,
    FocusConfig,
    FocusedRowAttachmentOptions,
    FocusHighlightConfig,
    FocusHighlightProps,
    FocusState,
    FocusStrategyBase,
    FocusStrategyPlugin,
    GridCapabilities,
    GridCapability,
    GridFocusInternals,
    GridKeyHolder,
    GridLayersInternals,
    GridPlugin,
    GridPluginEntry,
    GridPluginInternals,
    GridRowsInternals,
    GridScrollInternals,
    GridStore,
    GridViewport,
    HFocusStyleName,
    HorizontalFocusAxisConfig,
    HorizontalScrollHooks,
    InitGridPlugin,
    KeyDelegate,
    KeyValue,
    PerfGridPlugin,
    PerfHandle,
    PerfPlugin,
    PluginCellTemplate,
    PoolBufferConfig,
    PooledNodeVM,
    PoolOps,
    PoolSizeResult,
    Position,
    PositionValue,
    RemoteKey,
    RowCardsWrap,
    RowCardsWrapContext,
    RowYOffsetProvider,
    SmoothScrollConfig,
    TxCounts,
    VerticalFocusAxisConfig,
    VerticalFocusOptions,
    VerticalFocusState,
    VerticalFocusStrategyPlugin,
    VerticalScrollHooks,
    VerticalStrategyConfig,
    VFocusStyleName,
} from "@roku-sdk/flex-grid";
import type { FixedFocusYParams } from "@roku-sdk/flex-grid/geometry";
import type {
    HStrategyExtension,
    HStrategyFocusContext,
    HStrategyMatcher,
    HStrategyScrollContext,
} from "@roku-sdk/flex-grid/compose-h-strategy";

Each is documented here once and linked from every page that uses it.

Types

CardLayoutContext

Context passed to optional per-card layout callbacks (e.g. zoom-focus plugin).

NameTypeDescription
cellHeightnumber
cellWidthnumber
colnumber
focusedboolean
rownumber
templateIdstring

CardPool

NameTypeDescription
isWarmupCompletebooleanTrue after warmupFromConfig or markWarmupComplete has run.
sizenumber
acquire(templateType: string) => PooledNodeVM<T>
bindHost(ref: ContainerRef) => void
dispose() => void
markWarmupComplete() => void
release(vm: PooledNodeVM<T>, templateType: string) => void
setGridKey(key: object) => void
setNodeOwner(owner: Owner | null) => void
warmup(templateType: string, count: number) => void
warmupFromConfig(config: Record<string, number>) => void

CellRenderProps

Props passed to the getCellComponent factory for each visible pool node.

NameTypeDescription
colnumberData column index of this cell.
focusedAccessor<boolean>Reactive accessor — true when this cell has focus.
gridKey?objectStable per-grid instance key — use as a WeakMap key when cell templates need per-grid plugin state (e.g. spotlight deferred-col bridges).
heightnumberCell height in pixels (from cellSize or rowCellSize).
isActiveAccessor<boolean>Reactive accessor — true while this pool node is on-screen (acquired). False when parked in the pool. Templates can use this to skip animations (e.g. snap scale instead of tweening off-screen nodes).
itemT | undefinedThe item for this cell, or undefined when the row is still loading.
rownumberData row index of this cell.
setFocusBoundary(focusId: object | null) => voidSee below.
setKeyDelegate(delegate: KeyDelegate | null) => voidRegister a KeyDelegate for this cell. See below.
widthnumberCell width in pixels (from cellSize or rowCellSize).

CellRenderProps.setFocusBoundary

Register this cell's own FocusBoundary identity, for composite cells that would rather receive keys through the focus chain than through a KeyDelegate.

While this cell is the focused cell, the grid moves its own boundary's virtual focus to the identity registered here. Keys then reach the cell's boundary first, and the grid's navigation runs only when that boundary declines — a key the cell resolves internally (zone move, inner grid movement, Select) never reaches the grid, and a key it has no neighbour for falls through to move the grid. This inverts the KeyDelegate contract, where the delegate runs before navigation and has to return false to hand the key back.

Requires the grid to be in boundary mode (a focusId on the grid config); without one there is no boundary whose focus to move. Pass null to unregister.

The identity must be registered by a real FocusBoundary (or useFocusable) in the cell's subtree. Unlike CellRenderProps.setKeyDelegate, this registration is not cleared when the pool node is recycled: a pool node's subtree mounts once and is reused, so its boundary — and therefore its identity — is stable for the life of the grid, and clearing it would leave the cell unreachable after its first reuse. Register once and let it stand.

Because that boundary outlives the item it first mounted for, drive its contents from row / col / item rather than from mount-time values, and prefer rememberLastFocus={false} so sub-focus resets when the cell is reused. rememberLastFocus remembers per pool node, not per item, so a recycled node reopens on whatever the previous item last focused. To remember per item, store the last sub-focus on the item and derive initialFocus from it.

Shared pools are supported when all grids share the same root FocusBoundary. The grid rebinds the cell boundary's parent to the displaying grid before giving it focus.

CellRenderProps.setKeyDelegate

Register a KeyDelegate for this cell. The delegate is called before the grid's own navigation handler so the cell can intercept keys. Return false from handleKey to fall through to the grid. Set null to unregister (e.g., when the cell collapses). The delegate is automatically cleared when the pool node is recycled.

Only movement / RemoteKey.Select keys claimed by the grid reach the delegate — see KeyDelegate.handleKey.

CellVAlign

Vertical alignment for cells with per-item heights within a row.

type CellVAlign = "top" | "center" | "bottom";

FixedFocusYParams

Describes viewport and row geometry for a fixed vertical focus strategy.

Row indices are zero-based integers and all dimensions are in pixels. Empty content uses a zero row count; row accessors are not called for it.

Also exported as FixedFocusYParams from @roku-sdk/flex-grid (deprecated).

Deprecated: FixedFocusYParams. Import from @roku-sdk/flex-grid/geometry instead. The root export will be removed in the next major version.

Example

const geometry: FixedFocusYParams = {
    containerY: 50, viewportHeight: 200, numRows: 4,
    getRowY: row => row * 100, getRowHeight: () => 100
};
NameTypeDescription
containerYnumberY offset where the grid container starts on screen, in pixels.
getRowHeight(r: number) => numberReturns the height in pixels for a zero-based row index.
getRowY(r: number) => numberReturns the content Y position in pixels for a zero-based row index.
numRowsnumberNumber of rows in the grid.
viewportHeightnumberHeight of the viewport, a size measured from containerY, matching the grid's clip rect. The bottom edge in screen space is containerY + viewportHeight.

FlexGridClippingConfig

Clipping options — when enabled, width and height are required.

type FlexGridClippingConfig = { enableClipping: false; height: number; width: number } | { enableClipping: true; height: number; width: number };

FlexGridConfig

type FlexGridConfig<T> = FlexGridConfigCore<T> & FlexGridClippingConfig;

FlexGridConfigCore

Configuration fields shared by clipped and unclipped grids.

NameTypeDescription
allowJumpOnEmpty?boolean | Accessor<boolean>When false (default), jumpTo waits until rows 0..targetRow are present in the store and the target row has at least one item. When true, jump immediately; unloaded rows render placeholders in the viewport window.
captureKeyPress?booleanBoundary mode only (focusId set). See below.
cellSize[number, number]
cellSpacing?[number, number]
cellVAlign?CellVAlignVertical alignment for cells with per-item heights. Default: "bottom".
colWidths?number[]
debugViewport?booleanDebug: draw a rectangle outline around the grid viewport boundary. Useful for visualizing the scroll/windowing region. Default: false.
debugViewportColor?stringColor of the debug viewport outline (hex RGBA). Default: "0x00FF00FF" (green).
declineUnhandledNavigation?booleanBoundary mode only (focusId set). See below.
focusFocusConfigFocus and scroll strategies for horizontal and vertical axes.
focusId?objectIdentity object registering this grid as a nested FocusBoundary focusable inside an enclosing (root) FocusBoundary. See below.
getCellComponent(templateId: string) => Component<CellRenderProps<T>>
getCellTemplateId?(cellData: T | undefined, row: number) => string
handleAdditionalKeyEvents?RemoteKeyName[]See below.
id?stringSceneGraph id for the grid's root group, so the grid is identifiable when inspecting the UI tree (MCP app-ui, probe-node). Internal ge* node ids keep their own auto-generated per-instance prefix and are unaffected.
initialFocus?[number, number]
jumpTo?[number, number] | Accessor<[number, number] | undefined>Declarative focus/scroll target. See below.
numColsnumber
numRowsnumber
onEdgeReached?(edge: Position) => voidFires when the user was already at a grid boundary and pressed further — the cue to hand focus to a neighbouring region. Never fires on wrap. See Position.
onFocusChanged?(row: number, col: number, item: T | undefined) => void
onItemSelected?(row: number, col: number, item: T | undefined) => booleanFires on OK for the focused cell. Return true to consume the key; return false to decline so a parent grid can run its own onItemSelected (nested mode via forwardKeyEvent). When omitted, OK is treated as declined (bubbles).
onJumpToDataNeeded?(targetRow: number, targetCol: number) => voidCalled when a jump is deferred because rows 0..targetRow are not yet in the store. App should prefetch that range; grid retries on storeVersion bump. Fires at most once per deferred [targetRow, targetCol] until the jump succeeds or the target changes (no repeat calls on each storeVersion bump).
onUnhandledAdditionalKey?(key: RemoteKeyName, press: boolean) => voidSee below.
plugins?GridPlugin<T>[]Tree-shakeable grid plugins — bob, row-titles, focus-highlight, perf, and custom extensions. See below.
pool?CardPool<T>External shared pool. When provided, the grid binds the host and applies poolBuffer warmup on mount (same as the internal pool path). Caller owns pool.dispose().
poolBuffer?PoolBufferConfig
rowCellWidths?number[]Per-row cell widths for focus highlight sizing. If provided, overrides cellSize[0] for focus width.
rowHeights?number[]
rowHGap?number[]Per-row horizontal gap between cells. Falls back to cellSpacing[0] when omitted.
rowNumCols?number[]Per-row column count override. When provided, rows[r] has rowNumCols[r] logical columns. Rows whose item count equals rowNumCols[r] are treated as "full" (and wrap in fixedFocusWrap). Falls back to numCols when not provided.
smoothScroll?boolean
smoothScrollConfig?{ focusSettleEase: "linear" | "outCubic"; holdDelay: number; msPerStep: number }
translation?[number, number]
viewportBuffer?{ bottom: number; left: number; right: number; top: number }See below.

FlexGridConfigCore.captureKeyPress

Boundary mode only (focusId set). When true, a key this grid consumed on the key-down is bound to the grid, so its key-up is delivered here no matter where focus has moved in the meantime.

Smooth scroll is the reason it exists. The grid arms its hold timer on the press and only settles when it sees the release, but a glide moves virtual focus as it travels: cross a row whose cell registered its own boundary via setFocusBoundary, and the key-up is offered to that cell first. A cell that owns the key on both edges consumes it there, the grid never learns the hold ended, and the glide runs to the end of the data.

This is not limited to the directions. Capture keys off "the grid consumed this press", so it covers every key the grid's boundary claims: the four directions, Select (OK), Fwd / Rev, and everything in FlexGridConfigCore.handleAdditionalKeyEvents. For those, the visible effect is that if focus moved between the press and the release, the release comes back to the grid rather than descending to the newly focused cell. With FlexGridConfigCore.declineUnhandledNavigation on, a press the grid declined is not captured, so only keys it actually acted on are bound.

A cell that declines the key-up does not need this — that path already works, and is what the in-repo examples rely on. Turn it on when the grid holds rows whose cells claim movement keys, or when you would rather not depend on every cell template getting the decline right.

A bound key is released when the grid loses focus, so handing focus to another region mid-hold does not leave the key claimed — which also bounds the case where the key-up never arrives at all. Nothing is synthesized either way.

Implemented by captureKeyPress on the boundary the grid renders; see IFocusBoundaryProps.captureKeyPress in @roku-sdk/navigation for the exact semantics.

FlexGridConfigCore.declineUnhandledNavigation

Boundary mode only (focusId set). When true, keys the grid's navigation did not act on are declined instead of swallowed, so they bubble to whatever encloses the grid — a parent cell's own FocusBoundary, or the app's root boundary.

A grid's boundary otherwise consumes every movement key it claims, whether or not focus moved, which leaves an inner grid unable to hand its top / bottom / right edges to the grid around it: onEdgeReached fires, but the key is already gone. Declining removes the need for the out-of-band edge flag that pattern otherwise requires.

Consumption is judged the same way as bare mode's forwardKeyEvent: true when the focused cell's delegate consumed the key, when focus moved, or when onItemSelected returned true. That includes OK — with this on, an onItemSelected returning false reaches the enclosing region instead of being dropped.

Off by default: a top-level grid that declines its edge keys would start handing them to the app's root boundary, which is a behaviour change for existing apps.

FlexGridConfigCore.focusId

Identity object registering this grid as a nested FocusBoundary focusable inside an enclosing (root) FocusBoundary. When provided, the grid owns its movement keys (up/down/left/right/OK) through that boundary and hands region focus back to siblings via FlexGridConfigCore.onEdgeReached + setFocus.

Omit for a nested/bare grid (e.g. a grid inside another grid's cell): no boundary is created and keys arrive only via FlexGridHandle.forwardKeyEvent (driven by the parent cell's KeyDelegate).

FlexGridConfigCore.handleAdditionalKeyEvents

Extra keys to route into the grid's key pipeline, on top of the built-in movement and selection keys (BOUNDARY_KEYS: RemoteKey.Left / Right / Up / Down / Select / Fwd / Rev).

Listed keys are added to the grid's nested FocusBoundary claimed-key set and are offered to the focused cell's KeyDelegate first, matching the normal pipeline order. The grid itself never navigates on these keys.

If the delegate declines (returns false), the key is re-thrown to the enclosing boundary via FlexGridConfigCore.onUnhandledAdditionalKey so opting a key in never traps it.

Also honored by FlexGridHandle.forwardKeyEvent so nested/bare grids get the same behavior.

Treat this as static configuration. The claimed-key set is fixed when the grid's FocusBoundary is created. Via defineFlexGrid the array participates in the remount dependency list, so a content change remounts the grid; calling createFlexGrid directly there is no remount machinery at all and a later change has no effect. If the set needs to vary, opt in to the union of every key you might need and decide per-event inside the KeyDelegate.

FlexGridConfigCore.jumpTo

Declarative focus/scroll target. Reactive when passed as an Accessor. First application to a non-(0,0) position snaps (no tween); later changes animate. Supersedes initialFocus when both are set.

Dedup: the internal jump effect skips when the clamped target equals the last successfully applied declarative jump. Re-setting the same [row, col] after the user navigates away with the D-pad is a no-op. Use FlexGridHandle.jumpTo for unconditional re-jumps, or clear the accessor to undefined before re-assigning.

FlexGridConfigCore.onUnhandledAdditionalKey

Called when a key listed in FlexGridConfigCore.handleAdditionalKeyEvents was claimed by the grid's boundary but declined by the focused cell's KeyDelegate.

Because the grid's boundary claims these keys, they no longer reach the enclosing boundary's onUnhandledKey on their own. Use this hook to run the app-level fallback (the behavior the key would have had if it were never opted in).

Only fires in top-level (focusId) mode. In bare/nested mode the same condition is surfaced by FlexGridHandle.forwardKeyEvent returning false.

Fires for both press and release, mirroring the raw boundary events — guard on press if the action should run once per keypress.

Captured once at creation (like FlexGridConfigCore.onEdgeReached and FlexGridConfigCore.onItemSelected), so a later identity change is not picked up. Read changing state from a signal inside the callback rather than closing over it.

FlexGridConfigCore.plugins

Tree-shakeable grid plugins — bob, row-titles, focus-highlight, perf, and custom extensions.

Scale-on-focus is a template concern — wrap cell templates with withZoomFocus from @roku-sdk/flex-grid/zoom-focus instead of a plugin.

FlexGridConfigCore.viewportBuffer

Viewport buffer for row/column windowing (all values in pixels).

  • top: extra pixels to render above viewport (default: 500)
  • bottom: extra pixels to render below viewport (default: 500)
  • left: extra pixels to render left of viewport (default: 500)
  • right: extra pixels to render right of viewport (default: 500)

FlexGridHandle

NameTypeDescription
elementElementThe JSX element to render
focusedCol() => number
focusedItem() => T | undefined
focusedRow() => number
storeGridStore<T>Direct reference to the underlying store. See below.
forwardKeyEvent(key: RemoteKeyName, press: boolean) => boolean
jumpTo(row: number, col: number, animate?: boolean) => boolean
resetRowPosition(row: number) => void
setNumRows(n: number) => void
setRowGeometry(patch: { rowCellWidths: number[]; rowHeights: number[]; rowHGap: number[]; rowNumCols: number[] }) => void
setRowHeight(row: number, height: number) => void
setRowHeights(heights: number[]) => void
updateRows(startRow: number, rows: T[] | undefined[]) => void

FlexGridHandle.store

Direct reference to the underlying store. Reading is always safe.

Writing to store.rows bypasses the emptiness bookkeeping behind the loaded-empty row collapse, so a row published that way keeps its full height even when set to []. Publish through FlexGridHandle.updateRows / FlexGridHandle.setNumRows instead.

FocusConfig

Focus and scroll behavior for both axes — pass on createFlexGrid({ focus }).

NameTypeDescription
horizontalHorizontalFocusAxisConfig
verticalVerticalFocusAxisConfig

FocusedRowAttachmentOptions

Options for GridRowsInternals.attachToFocusedRow.

NameTypeDescription
localY?(row: number) => numberLocal Y (px) for the node inside the focused row slot's nodeRef; e.g. getRowHeight(row) to sit under the row's cards, or a negative height to sit above them. Default 0.

FocusHighlightConfig (@roku-sdk/flex-grid)

Deprecated.

Legacy root type; import FocusHighlightConfig from @roku-sdk/flex-grid/focus-highlight instead. Will be removed from the public exports in the next major version.

Legacy focus-highlight configuration retained for compatibility.

NameTypeDescription
borderWidth?number
color?string
cornerRadius?number

FocusHighlightProps

NameTypeDescription
heightnumberHeight of the currently-focused cell.
widthnumberWidth of the currently-focused cell.
xnumberX position of the focus highlight in screen coordinates.
ynumberY position of the focus highlight in screen coordinates.

FocusState

State passed to horizontal strategy functions on each focus update.

NameTypeDescription
colnumberCurrent (virtual) focused column index. May exceed numCols for wrap strategies.
numColsnumberTotal number of columns in the grid store (global, not per-row). For the actual item count of the current row, see rowItemCount.
rownumberRow index this state is computed for.
rowItemCount?numberActual number of items in the current row. May be less than numCols when rows have variable item counts. Defaults to numCols if not provided.
screenWidthnumberScreen width in pixels (typically 1920).

FocusStrategyBase

Base config shared by all horizontal focus strategies.

NameTypeDescription
focusOffsetnumberX offset from the left screen edge where the focused item appears.

FocusStrategyPlugin

Plugin interface for horizontal focus strategies. Each strategy is a separate module; only imported strategies end up in the bundle.

NameTypeDescription
namestring
computeFocusX(state: FocusState, config: TConfig, cumulativeX: number[]) => number
computeScrollX(state: FocusState, config: TConfig, cumulativeX: number[]) => number
resolveColIndex?(virtualCol: number, numCols: number) => number
usesDirectScrollX?(state: FocusState) => boolean

GridCapabilities

Checks statically declared behavior before a plugin mounts duplicate chrome. The lookup includes every plugin's provides declarations, including the caller. It is fixed at grid construction and is not a reactive accessor.

NameTypeDescription
has(capability: "focusVisual") => booleanTrue when some plugin in plugins[] declared capability in its provides.

GridCapability

Behaviour a plugin declares it owns, so other plugins can stand down instead of drawing the same thing twice. Declared statically via provides on the plugin object (read before any init runs, so it never depends on plugin order).

  • focusVisual; a plugin draws the focused cell's visual treatment.
    createFocusHighlightPlugin suppresses its own node when something else provides it.

No first-party plugin declares focusVisual today: this is a third-party extension point for plugins that draw their own focus chrome. Cell templates cannot declare capabilities, so a template that owns the visual (via withZoomFocus, say) has the app pass renderNode: false to createFocusHighlightPlugin instead. Both routes reach the same outcome; the highlight still feeds per-cell focused to the engine, it just does not draw.

type GridCapability = "focusVisual";

GridFocusInternals

Provides the focused item and geometry for focus-following plugin visuals. Read position, item and size accessors inside effects or JSX so visuals update as focus moves; values read once during init are only snapshots.

NameTypeDescription
cellHeight() => numberTarget height of the focused cell (not tweened).
cellWidth() => numberTarget width of the focused cell (not tweened).
col() => numberFocused column index (data col, not virtual).
highlightLocalX() => numberX translation (content-space, inside the scrolling content container) for the focus highlight.
highlightLocalY() => numberY translation (content-space, inside the scrolling content container) for the focus highlight, before row Y offsets. Add rows.rowYOffset(focus.row()) to follow a row offset that pushes the focused row's cards down.
item() => T | undefinedData item at the current focus position.
notifySettleEnd() => voidCalled by the focus-highlight plugin's position tween when it stops and no smooth scroll is in progress. Clears settling and fires the vertical focus-settle-end scroll hook.
row() => numberFocused row index.
settleEase"linear" | "outCubic"Ease used for the focus-highlight glide when a held smooth scroll releases.
settleStop() => [number, number][]Candidate stop used to restart the highlight glide with a non-linear settle ease, in the vector proposedStops form: a one-entry list holding the [x, y] destination, so both components settle. Empty when no settle is pending.
settling() => booleanTrue while the focus highlight glides to the focused item after a held smooth scroll releases.
setVisibility(accessor: () => boolean) => voidPush the focus-visibility accessor to the grid. The engine reads it to clear per-cell focused signals (in-cell outlines, zoom scale) while internal row/col focus is preserved. Defaults to always-visible when no focus-highlight plugin is present.

GridKeyHolder

Mutable holder so external pools can bind gridKey after warmup.

type GridKeyHolder = {
    current?: object;
};
NameTypeDescription
current?object

GridLayersInternals

Mounts plugin-owned components above, below or alongside the grid's content. Use scroll layers for content-space visuals and mountOverlay for an overlay outside scrolling content. Read refs and other grid accessors inside effects or JSX; the viewport object contains fixed construction-time geometry.

NameTypeDescription
mountOverlay(overlay: Component) => () => voidMount a reactive overlay component in the grid's main render tree. Use for plugin-owned nodes and for tweens targeting nodes inserted imperatively. Pass a Solid component (not a render callback) so signal reads stay tracked. Returns a dispose function called on plugin cleanup.
mountScrollLayerContent(layer: "front" | "back", overlay: Component) => () => voidMount a reactive overlay into a scroll layer that inherits the grid scroll. Idempotently requests the layer. Returns a dispose function called on plugin cleanup. Use for the focus-highlight node.
requestScrollBehindLayer() => voidIdempotent; mounts the behind layer on next render.
requestScrollFrontLayer() => voidIdempotent; mounts the front layer on next render.
scrollBehindGroupRef() => ContainerRef | nullBehind stacking layer behind the main content. Opt-in via requestScrollBehindLayer().
scrollFrontGroupRef() => ContainerRef | nullFront stacking layer. Opt-in via requestScrollFrontLayer().
scrollGroupRef() => ContainerRef | nullMain scroll layer; row slots and default plugin inserts.
viewportGridViewportThe grid's viewport. Scroll-layer content at x = -viewport.x sits on the left edge of the grid's parent.

GridPlugin

Plugin entry accepted by FlexGridConfig.plugins. Holds two kinds:

  • Lifecycle (InitGridPlugin<T>) — bob, row-titles, focus-highlight, custom plugins.
    Implement init(internals) to mount UI or register scroll hooks.
    When writing custom plugins, extend or return InitGridPlugin<T>,
    not GridPlugin<T> — GridPlugin is the array-element union type.

  • Perf (PerfPlugin) — pass createPerfPlugin().
    Extracted before engine/nav creation by resolveGridPlugins.

Scale-on-focus is a template concern — use withZoomFocus from @roku-sdk/flex-grid/zoom-focus on cell templates instead of a plugin.

Example

// Implementing a custom init plugin:
import type { InitGridPlugin, GridPluginInternals } from "@roku-sdk/flex-grid";

function createMyPlugin<T>(): InitGridPlugin<T> {
  return {
    init(internals: GridPluginInternals<T>) { ... }
  };
}

// Passing plugin kinds:
plugins: [
  createPerfPlugin({ output: "both" }),
  createMyPlugin(),
]
type GridPlugin<T> = GridPluginEntry<T>;

GridPluginEntry

All plugin kinds accepted in FlexGridConfig.plugins.

type GridPluginEntry<T = unknown> = InitGridPlugin<T> | PerfGridPlugin;

GridPluginInternals

Grid state and registration hooks supplied to InitGridPlugin.init. Use these namespaces to register chrome, follow focus or coordinate animation without accessing the engine implementation. The engine exists during init, but engineReady() becomes true only after every plugin has initialized. All accessors are reactive; read them inside effects or JSX props. The namespace objects themselves are stable, so const { rows, focus } = internals at the top of init is safe.

NameTypeDescription
capabilitiesGridCapabilitiesWhat other plugins in plugins[] declared they own.
engineReady() => booleanTrue once the engine has been initialised.
focusGridFocusInternals<T>Focus position, focused-cell geometry, and focus-highlight glide state.
gridKeyobjectStable per-grid instance key; share with cell templates via CellRenderProps.gridKey for per-grid plugin state (WeakMap lookups).
layersGridLayersInternalsScroll layers and overlay mounting.
nodeIdPrefixstringAuto-generated per createFlexGrid (fg0_, fg1_, ...); matches engine row-slot ids. Prefix any fixed SceneGraph id in plugin JSX so nested grids stay unique.
rowsGridRowsInternals<T>Row data, geometry, and per-row extension points.
scrollGridScrollInternalsScroll state and lifecycle hook registration.

GridRowsInternals

Provides row geometry and registration hooks for row chrome or custom layout. Read reactive accessors such as count, rowYOffset and snapping inside effects or JSX. Row renderers receive a recycled slot's reactive row accessor; do not cache its current value when registering the renderer.

NameTypeDescription
attachToFocusedRow(node: () => unknown, options: FocusedRowAttachmentOptions) => () => voidRe-parent a plugin-owned node into the focused row's slot, so it tracks that row's Y tween and scroll exactly with no follow tween of its own. The node moves only when the focused row changes. Mount the node itself first (e.g. via layers.mountOverlay) and pass its ref accessor. Returns a dispose function; also disposed on plugin cleanup.
count() => numberTotal number of rows (reactive).
getRowHeight(r: number) => numberHeight of row r.
getRowNumCols(r: number) => numberLogical column count for row r.
getRowY(r: number) => numberY position of row r inside the scroll container (content-space), before row Y offsets.
registerCardLayout(handler: (vm: PooledNodeVM<T>, ctx: CardLayoutContext) => void) => voidRegister a per-card layout callback; invoked by the engine on every layout pass after position/focus updates. See below.
registerRowChrome(render: (row: Accessor<number>) => unknown, layer: "front" | "back") => voidRegister a per-row chrome renderer. See below.
registerRowYOffset(provider: RowYOffsetProvider) => () => voidRegister a reactive per-row Y offset; the engine adds it to each row's target Y, and the per-row Y tween glides rows to the new pitch when the offset changes. See below.
requestLayout() => voidRe-run card layout for visible pool nodes (e.g. after plugin gap geometry changes).
reservedRowTopnumberHeight reserved above every row for plugin chrome such as titles, already inside the row gap.
rowYOffset(row: number) => numberSum of every registered row Y offset for row at the current focus (reactive, including to providers registered after the caller first read it).
snapping() => booleanTrue while the grid snaps its row slots instead of gliding them; after a non-animated jumpTo, or when a row collapsing or expanding reflows the rows below it. A plugin node that moves with the rows should snap too while this holds.
storeGridStore<T>Live store reference; rows and dirtyRows.

GridRowsInternals.registerCardLayout

Register a per-card layout callback; invoked by the engine on every layout pass after position/focus updates. Handlers run in registration order (call order during init).

Call this synchronously from init (not from createEffect, queueMicrotask, or other deferred work), or the first layout pass may run before your handler is registered.

GridRowsInternals.registerRowChrome

Register a per-row chrome renderer. render receives a reactive row accessor; read it inside JSX props or effects, never destructure at the top level. The output is mounted into every row slot's back or front chrome container and re-used as slots are recycled (the row accessor updates reactively). Must be called synchronously from init, exactly once per layer; calling it multiple times for the same layer adds additional chrome without removing previous mounts. layer: "front" (default) renders in front of cards; "back" renders behind them.

GridRowsInternals.registerRowYOffset

Register a reactive per-row Y offset; the engine adds it to each row's target Y, and the per-row Y tween glides rows to the new pitch when the offset changes. Use it to open space between rows (e.g. a metadata block below the focused row).

Drive it from a reactive scope: read the plugin's own createMemo inside the provider so the engine re-lays-out when the offset changes. See RowYOffsetProvider for the purity requirement. Returns a dispose function; also disposed on plugin cleanup.

GridScrollInternals

Coordinates plugin animation with navigation, smooth scrolling and settling. Read scroll-state accessors inside effects or JSX. Register lifecycle callbacks synchronously in init; callbacks run on events rather than as reactive effects.

NameTypeDescription
isSmoothScrollingX() => booleanTrue while horizontal smooth scroll is in progress.
overrideActive() => booleanTrue while a programmatic scroll override (e.g. jumpTo) is in flight; size tweens use a linear ease.
registerHorizontalHooks(hooks: HorizontalScrollHooks) => voidRegister horizontal scroll callbacks; invoked when the row X tween settles. Every plugin's hooks fire, in registration order.
registerVerticalHooks(hooks: VerticalScrollHooks) => voidRegister vertical navigation callbacks; invoked by the nav controller. Every plugin's hooks fire, in registration order.
smoothScrolling() => booleanTrue while a held smooth scroll (horizontal or vertical) is in progress.
tweenDuration() => numberCurrent per-step tween duration (seconds).

GridStore

Flat mutable data store for the grid.

  • rows[rowIndex] holds the items for that row.
    • T[] → row has data, renders cells normally
    • undefined → row is loading; placeholder/skeleton shown; height preserved
    • [] → row has no items; collapses to zero height
  • dirtyRows tracks which rows changed since the last engine tick.
  • version is bumped on every mutation and drives the monolithic reactive memo.
  • forceFullRelayout is set by setNumRows so the engine cannot take the
    data-only short-circuit until the next full layout pass clears it.
NameTypeDescription
dirtyRowsSet<number>
forceFullRelayoutboolean
rowsT[] | undefined[]
versionnumber

GridViewport

The grid's viewport, in the coordinates config.translation is given in.

NameTypeDescription
clippedbooleanTrue when enableClipping cuts everything the grid draws to this rect.
heightnumberViewport height in pixels.
widthnumberViewport width in pixels.
xnumberLeft edge in pixels in the parent coordinate system.
ynumberTop edge in pixels in the parent coordinate system.

HFocusStyleName

type HFocusStyleName = "fixedFocus" | "floatingFocus" | "fixedFocusWrap" | "zoomFocus";

HorizontalFocusAxisConfig

Horizontal focus axis: strategy plugin plus its tunables.

NameTypeDescription
options?Partial<TConfig>
strategyFocusStrategyPlugin<TConfig>

HorizontalScrollHooks

Horizontal scroll lifecycle callbacks that plugins can register.

NameTypeDescription
onHorizontalNav?() => voidCalled when a left/right key press moves focus to a new column.
onHorizontalScrollEnd?() => voidCalled when the focused row's X scroll tween finishes and H smooth scroll is idle.
onHorizontalSmoothScrollStart?() => voidCalled when held horizontal smooth scroll starts.

HStrategyExtension

Partial override applied on top of a base horizontal focus strategy. Provide only the hooks you need; unmatched states delegate to the base.

NameTypeDescription
matchesHStrategyMatcherReturns true when this extension owns the current focus state.
computeFocusX?(ctx: HStrategyFocusContext<TConfig>) => number
computeScrollX?(ctx: HStrategyScrollContext<TConfig>) => number

HStrategyFocusContext

H Strategy Focus Context configuration and callbacks.

NameTypeDescription
baseFocusXnumberValue from the base strategy before extensions run.
configTConfigConfiguration of the base focus strategy.
cumXnumber[]Cumulative column positions in pixels.
stateFocusStateCurrent focus indices and navigation direction.

HStrategyMatcher

Returns true when an extension applies to the current focus state.

type HStrategyMatcher = (state: FocusState) => boolean;

HStrategyScrollContext

H Strategy Scroll Context configuration and callbacks.

NameTypeDescription
baseScrollXnumberValue from the base strategy before extensions run.
configTConfigConfiguration of the base focus strategy.
cumXnumber[]Cumulative column positions in pixels.
stateFocusStateCurrent focus indices and navigation direction.

InitGridPlugin

Registers custom grid chrome and behavior through GridPluginInternals.

Pass a plugin object in createFlexGrid({ plugins: [plugin] }). The grid reads static contributions (cellTemplate, reservedRowTop, wrapRowCards and provides) during construction, before the pool and row slots are created. It calls init once per mounted grid, in plugins array order, after creating the engine and navigation controller. engineReady() is still false during init and becomes true after all plugins have initialized.

Register layout and scroll hooks synchronously inside init, before the first layout pass. init returns nothing: returning a disposer does not register cleanup. Use onCleanup from @roku-sdk/solid for subscriptions, timers and disposers returned by registration or mounting methods. Effects created in init belong to the grid's reactive owner and stop when that owner is disposed. Mounted components also unmount with the grid. Calling a returned disposer removes that registration or mount earlier; register it with onCleanup when you want explicit teardown. Shared external pools remain caller-owned.

Capabilities describe behavior supplied by the complete plugin set, not permission to access internals. Declare ownership statically with provides; the grid collects all declarations before calling any init. Query internals.capabilities.has("focusVisual") to avoid duplicate focus chrome, regardless of plugin order. focusVisual is currently the only capability. A plugin querying its own declared capability also receives true. It does not enforce exclusive ownership: cooperating plugins decide whether to stand down.

Read accessors inside effects or JSX properties to track changes. Namespace objects are stable, but caching focus.row() in init captures only its initial value. This example draws an outline in content coordinates so it inherits scrolling; it adds any plugin-provided row offset to the highlight position.

Example

import { Rectangle } from "@roku-sdk/components";
import { onCleanup } from "@roku-sdk/solid";
import { createFlexGrid, fixedFocusStrategy, type InitGridPlugin } from "@roku-sdk/flex-grid";

const outline: InitGridPlugin<string> = {
    provides: ["focusVisual"],
    init(internals) {
        const { focus, rows, layers } = internals;
        const dispose = layers.mountScrollLayerContent("front", () => (
            <Rectangle
                translation={[
                    focus.highlightLocalX(),
                    focus.highlightLocalY() + rows.rowYOffset(focus.row())
                ]}
                width={focus.cellWidth()}
                height={4}
                color="0xffffffff"
            />
        ));
        onCleanup(dispose);
    }
};

const grid = createFlexGrid<string>({
    numRows: 1,
    numCols: 2,
    cellSize: [240, 160],
    width: 1280,
    height: 720,
    focus: {
        horizontal: { strategy: fixedFocusStrategy },
        vertical: { strategy: fixedFocusStrategy }
    },
    plugins: [outline],
    getCellComponent: () => () => <Rectangle width={240} height={160} />
});
grid.updateRows(0, [["One", "Two"]]);
// Render grid.element inside your app's reactive component.
NameTypeDescription
cellTemplate?PluginCellTemplate<T>A pooled cell template this plugin supplies (e.g. a loading placeholder). Read once at grid construction, before the pool exists; must not change at runtime.
provides?"focusVisual"[]Behaviours this plugin owns, so other plugins can stand down (e.g. a plugin that scales the focused card declares "focusVisual" and the focus-highlight plugin then skips its own node). Collected before any init runs, so it does not depend on plugins[] order.
reservedRowTop?numberStatic pixels this plugin reserves above every row's cards for its chrome (e.g. a row title). Summed across plugins and folded into the engine's row pitch so cellSpacing[1] stays the residual gap between a row's chrome and the next row. Read once at grid construction; must not change at runtime.
wrapRowCards?RowCardsWrapWraps every row slot's cards in a plugin-owned node. Read once at grid construction, since row slots are allocated before any init runs; must not change at runtime.
init(internals: GridPluginInternals<T>) => void

KeyDelegate

NameTypeDescription
handleKey(key: RemoteKeyName, press: boolean) => boolean
onActivate?() => void
onDeactivate?() => void

KeyValue

A firmware key name as delivered by onKeyEvent(). Prefer the RemoteKey enum over string literals — its members are RCButton-aligned and its values are the runtime strings (notably RemoteKey.Select === "OK"). Plain strings remain assignable for forward compatibility with keys the enum does not name.

type KeyValue = RemoteKeyName;

PerfGridPlugin

Perf instrumentation plugin; discovered by kind before engine/pool creation.

type PerfGridPlugin = PerfPlugin;

PerfHandle

NameTypeDescription
layoutCountAccessor<number>Layout-effect evaluations counted in the most recent transaction.
poolOpsAccessor<PoolOps>
txCountsAccessor<TxCounts>
warmupCompleteAccessor<boolean>

PerfPlugin

NameTypeDescription
handlePerfHandle
kind"perf"
countAcquire() => void
countLayout() => void
countMiss() => void
countRelease() => void
dispose() => void
keypressEnd() => void
keypressStart(key: string | RemoteKeyNameCompat) => void
markWarmupComplete() => void

PluginCellTemplate

A cell template contributed by a plugin.

The grid folds these into cell routing ahead of the app's own getCellTemplateId, so the template gets a real pool key: nodes recycle per template and poolBuffer[id] warms them like any app template. Resolved before createPool, since the pool needs the component lookup at construction time.

NameTypeDescription
componentComponent<CellRenderProps<T>>Component rendered for cells this template claims.
idstringPool template key. Must not collide with a template the app returns from getCellTemplateId, or the two would share pooled nodes.
matches(cellData: T | undefined, row: number, col: number) => booleanDecides whether this template owns a given cell. See below.

PluginCellTemplate.matches

Decides whether this template owns a given cell. Called on every layout pass for every visible cell, so keep it cheap and free of side effects. Returning false defers to the next contributed template, and then to the app's getCellTemplateId.

row and col are the resolved data indices for the cell being laid out; col is the wrapped/virtualised column, matching the index used to read the item, so it is safe to route on. When several plugins contribute templates, the first match in plugins[] order wins.

PoolBufferConfig

Per-template pool warmup counts. Each key is a template ID; value is the number of nodes to pre-allocate.

type PoolBufferConfig = Record<string, number>;

PooledNodeVM

Reusable cell instance and setters supplied by a card pool to the grid.

NameTypeDescription
elementunknownJSX element — inserted into the render tree.
focusBoundaryIdobject | nullFocus-boundary identity registered by the cell template via CellRenderProps.setFocusBoundary. The grid parks its boundary's virtual focus here while this VM is the focused cell.
focusRelay?FocusRelay | nullStable parent context for the cell subtree; null or absent when the node was created outside a FocusBoundary. Optional so a hand-rolled CardPool keeps compiling: a VM without one simply cannot follow a shared pool between grids.
keyDelegateKeyDelegate | nullKey delegate registered by the cell template via CellRenderProps.setKeyDelegate.
templateTypestringTemplate type this node was created for — used by the pool for correct release routing.
setCol(c: number) => void
setFocused(f: boolean) => void
setIsActive(active: boolean) => void
setItem(item: T | undefined) => void
setRow(r: number) => void
setX(x: number) => void
setY(y: number) => void

PoolOps

NameTypeDescription
acquiresnumber
missesnumber
releasesnumber

PoolSizeResult

Deprecated.

Pool sizing is managed by the grid implementation. The root export will be removed in the next major version.

NameTypeDescription
poolHnumber
poolWnumber
totalnumber

Position

A boundary of the grid. Reported by FlexGridConfigCore.onEdgeReached when the user was already at that boundary and pressed further, which is the usual cue to hand focus to a neighbouring region.

Member values are the raw strings the callback has always received, so Position.Top === "top".

MemberValueDescription
Top"top"
Bottom"bottom"
Left"left"
Right"right"

PositionValue

The type FlexGridConfigCore.onEdgeReached reports, named separately for handlers that annotate their parameter. An alias of Position, so a received value works either way: edge === Position.Top and edge === "top" both compile and both match, and a handler annotated (edge: "top" | "bottom" | "left" | "right") stays assignable. The one direction that does not typecheck is passing a bare literal into a parameter of this type — use the member.

type PositionValue = Position;

RemoteKey

RCButton-aligned remote key names delivered by the firmware via onKeyEvent().

Enum member names follow the firmware RCButton identifiers. Enum values are the runtime key strings the app receives — NOT the ECP wire names used by tooling (e.g. ECP sends "Rev" but the app receives "rewind").

Note: Home is excluded because it is not delivered to the app.

RCButton EnumRuntime Key (RemoteKey value)Availability
Back"back"Available on all remotes
Up"up"Available on all remotes
Down"down"Available on all remotes
Left"left"Available on all remotes
Right"right"Available on all remotes
Select"OK"Available on all remotes
InstantReplay"replay"Available on all remotes
Play"play"Available on all remotes
Rev"rewind"Available on all remotes
Fwd"fastforward"Available on all remotes
Info"options"Available on all remotes
Backspace"backspace"Only available on some remotes in select markets
Search"search"Only available on some remotes in select markets
Enter"enter"Only available on some remotes in select markets
A"A"Only available on some remotes in select markets
B"B"Only available on some remotes in select markets
PlayOnly"playonly"Only available on some remotes in select markets
Stop"stop"Only available on some remotes in select markets
Guide"guide"Only available on some remotes in select markets
ChannelUp"channelup"Only available on some remotes in select markets
ChannelDown"channeldown"Only available on some remotes in select markets
LastChannel"lastchannel"Only available on some remotes in select markets
Partner"partner"Only available on some remotes in select markets
Red"red"Only available on some remotes in select markets
Green"green"Only available on some remotes in select markets
Yellow"yellow"Only available on some remotes in select markets
Blue"blue"Only available on some remotes in select markets
Exit"exit"Only available on some remotes in select markets
TV"tv"Only available on some remotes in select markets
Pause"pause"Only available on some remotes in select markets
WakeWord"wakeword"Only available on some remotes in select markets
VirtualKeyboard"virtualkeyboard"Only available on some remotes in select markets

Example

// Enum members (RCButton-aligned names):
onKey: { [RemoteKey.Select]: onPress(() => select()) }

// Legacy runtime string literals remain valid:
onKey: { OK: onPress(() => select()) }
MemberValueDescription
Back"back"
Up"up"
Down"down"
Left"left"
Right"right"
Select"OK"
InstantReplay"replay"
Play"play"
Rev"rewind"
Fwd"fastforward"
Info"options"
Backspace"backspace"
Search"search"
Enter"enter"
A"A"
B"B"
PlayOnly"playonly"
Stop"stop"
Guide"guide"
ChannelUp"channelup"
ChannelDown"channeldown"
LastChannel"lastchannel"
Partner"partner"
Red"red"
Green"green"
Yellow"yellow"
Blue"blue"
Exit"exit"
TV"tv"
Pause"pause"
WakeWord"wakeword"
VirtualKeyboard"virtualkeyboard"

RowCardsWrap

Wraps a row slot's cards in a plugin-owned node; the one place a plugin can put a node that is an ancestor of the cards, and so does not travel with the row's horizontal scroll.

Declared statically on the plugin, because row slots are allocated while the grid is constructed, before any init runs. Applied in plugins[] order, first entry outermost.

type RowCardsWrap = (context: RowCardsWrapContext) => JSX.Element;

RowCardsWrapContext

What RowCardsWrap is handed for one row slot.

NameTypeDescription
cardsElementThe slot's cards node. Whatever the wrapper returns must contain it.
rowAccessor<number>Reactive index of the row this slot currently shows.
screenHeightnumberGrid viewport height.
screenWidthnumberGrid viewport width, for a wrapper needing a rect larger than anything on screen.

RowYOffsetProvider

Extra Y (px) a plugin adds to a row's slot position, on top of its natural layout Y. Called for each row in the live window on every layout pass, and on each probe of the variable-row-height binary search, so it must be cheap.

Must be a pure function of (row, focusedRow) plus the plugin's own reactive reserve accessor. Reading any other grid signal (scroll position, column, ...) widens the dependency set of the row-window memos and the layout effect, causing extra relayouts.

type RowYOffsetProvider = (row: number, focusedRow: number) => number;

SmoothScrollConfig

NameTypeDescription
easing?"linear" | "outCubic"
holdDelay?number
msPerStep?number

TxCounts

NameTypeDescription
createElementnumber
insertNodenumber
removeNodenumber
setPropertynumber
txTimenumberWall-clock duration (ms) of the most recent render-thread transaction — measured from the transaction start (keypress if one fired first, otherwise the transactionStart hook) to the pre-commit hook.

VerticalFocusAxisConfig

Vertical focus axis: strategy plugin plus its tunables.

NameTypeDescription
options?VerticalFocusOptions
strategyVerticalFocusStrategyPlugin

VerticalFocusOptions

Tunables for a vertical focus axis.

NameTypeDescription
focusOffset?numberY offset from the top of the grid viewport where the focused row is pinned.

VerticalFocusState

State passed to vertical strategy functions on each focus update.

⚠️ This object is a shared mutable scratch instance reused across calls. Implementations must NOT retain a reference to it — read all needed values synchronously within the callback and discard the reference.

NameTypeDescription
baseline?numberScroll baseline used by floating-focus style strategies. When omitted, strategies should use their initial landing baseline.
numRowsnumber
rownumber

VerticalFocusStrategyPlugin

Customizes vertical scrolling and the focus highlight's screen position.

Set this object as focus.vertical.strategy; it is not an InitGridPlugin entry in plugins. Call geometry helpers with the same content coordinates used by the row accessors. Strategy state is reused by the engine, so consume it synchronously rather than retaining it. This strategy starts the focused row at the viewport top until the final rows need end-zone clamping.

Example

import type { VerticalFocusStrategyPlugin, VerticalFocusState,
    VerticalStrategyConfig } from "@roku-sdk/flex-grid";
import { computeScrollYForRow } from "@roku-sdk/flex-grid/geometry";

const rowY = (row: number, config: VerticalStrategyConfig) =>
    config.getRowY?.(row) ?? row * (config.rowPitch ?? 0);
const scrollY = (state: VerticalFocusState, config: VerticalStrategyConfig) =>
    computeScrollYForRow(state.row, {
        containerY: config.viewportTop ?? 0,
        viewportHeight: config.viewportHeight,
        numRows: state.numRows,
        getRowY: row => rowY(row, config),
        getRowHeight: row => config.getRowHeight?.(row) ?? config.rowPitch ?? 0
    });

const topAlignedStrategy: VerticalFocusStrategyPlugin = {
    name: "topAligned",
    computeScrollY: scrollY,
    computeFocusY: (state, config) => rowY(state.row, config) + scrollY(state, config)
};
// Pass { strategy: topAlignedStrategy } as focus.vertical.
NameTypeDescription
namestring
computeFocusY(state: VerticalFocusState, config: VerticalStrategyConfig) => number
computeScrollY?(state: VerticalFocusState, config: VerticalStrategyConfig) => number

VerticalScrollHooks

Vertical scroll lifecycle callbacks that plugins can register.

NameTypeDescription
onVerticalFocusSettleEnd?() => voidCalled when the focus highlight finishes gliding after a held vertical smooth scroll.
onVerticalNav?() => voidCalled when a vertical key press moves focus to a new row.
onVerticalScrollEnd?() => voidCalled when vertical scroll session ends (key release without smooth scroll, or after settle).
onVerticalSmoothScrollStart?() => voidCalled when held vertical smooth scroll starts.

VerticalStrategyConfig

Layout geometry forwarded by the engine to vertical strategy methods on every call. Mirrors the role focus.horizontal.options plays for horizontal strategies.

NameTypeDescription
focusOffsetnumberScreen Y where the focused row is pinned (fixedFocusY) or top of grid (floatingFocusY). See below.
getRowHeight?(row: number) => numberHeight of row r; used by floating-focus to detect off-screen overflow.
getRowY?(row: number) => numberY position of row r inside the scroll container (content-space).
rowPitch?numberDefault row pitch (height + gap) for uniform grids. Used as fallback when getRowY is absent.
viewportHeightnumberHeight of the viewport in pixels — a size, not a bottom edge. For a grid with a translation, this is the height of its band, so the band's bottom edge in screen space is viewportTop + viewportHeight.
viewportTop?numberScreen Y of the top of the viewport: the grid's own translation[1]. See below.

VerticalStrategyConfig.focusOffset

Screen Y where the focused row is pinned (fixedFocusY) or top of grid (floatingFocusY).

Unlike the horizontal focusOffset, which is relative to the grid's container, this one is in the grid's parent space: the engine adds translation[1] before calling the strategy. That is because computeScrollY returns the scroll group's own Y translation, and the scroll group sits at translation[1] when unscrolled. Horizontally the scroll group stays at translation[0] and rows scroll inside it, so computeScrollX works relative to it. Changing either convention would break existing custom strategies, so viewportTop is passed instead to reconcile this value with viewportHeight.

VerticalStrategyConfig.viewportTop

Screen Y of the top of the viewport: the grid's own translation[1]. Defaults to 0, which is what an untranslated grid has and what a strategy written before this field existed effectively assumed.

focusOffset is in screen space and viewportHeight is a size, so a strategy needs this to put the two in one space — the space of the available room below the focus line is viewportTop + viewportHeight - focusOffset. Reading viewportHeight as if it were the bottom edge is what made a translated grid scroll its rows above its own clip rect and render blank.

VFocusStyleName

type VFocusStyleName = "fixedFocus" | "floatingFocus";

Did this page help you?