Skip to content

Highlight Rendering: HighlightPath and HighlightOverlay

Focus Highlights explains how to customize the highlight a focusable Node shows — swapping in a HighlightFromNode, a raw Shape, or a Node of your own. This page covers the machinery underneath that actually draws those highlights: HighlightPath (from scenerystack/scenery), the Path subclass with the double-stroke "glow" look, and HighlightOverlay, the per-Display overlay that decides which highlight is currently active and renders it in its own child Display.

HighlightPath: the double-stroke look

A HighlightPath is a Path with a second, child Path (innerHighlightPath) drawn on top — an "outer" highlight (lighter, wider) and an "inner" highlight (darker, more opaque), giving the appearance of a highlight fading outward. HighlightFromNode (used throughout the Focus Highlights guide) is a HighlightPath subclass that derives its shape from a Node's bounds and keeps it updated as those bounds change.

ts
import { HighlightPath } from 'scenerystack/scenery';
import { Shape } from 'scenerystack/kite';

const highlight = new HighlightPath( Shape.roundRect( 0, 0, 40, 40, 4, 4 ), {
  dashed: true // e.g. to indicate the target is currently being manipulated
} );
OptionDefaultEffect
outerStroke / innerStrokeHighlightPath.OUTER_FOCUS_COLOR / INNER_FOCUS_COLORColors of the two strokes
outerLineWidth / innerLineWidthnull (computed from the local-to-global transform)Fix the line widths instead of letting them scale automatically with the highlighted Node
lineDashOverridenullFixes the dash pattern instead of letting it scale with the transform
dashedfalseWhether the highlight uses a dashed stroke — PhET convention for "this is currently grabbed/being manipulated"
transformSourceNodenullThe Node whose transform this highlight should track (set automatically by HighlightFromNode)

Static helpers worth knowing: HighlightPath.getDilationCoefficient( matrix ) computes how far outside a Node's bounds the highlight should sit so there's visible whitespace between the Node and the inner edge of the highlight, and HighlightPath.getDefaultHighlightLineWidth() returns the untransformed default line width — useful if you need to reserve layout space for a highlight.

HighlightOverlay: one per Display

Every Display that supports focus gets a HighlightOverlay, which owns a completely separate child Display layered on top of the main scene graph (pointer-events: none, so it never intercepts input). It listens to the global PDOM focus Property, the FocusManager's pointer/reading-block focus Properties, and picks one of four modes per activation:

ModeWhen
'bounds'Default — no custom focusHighlight was set; draws a HighlightFromNode around the focused Node's bounds
'shape'The Node's focusHighlight/interactiveHighlight is a kite Shape
'node'The Node's highlight is itself a Node (optionally focusHighlightLayerable to place it in the main scene graph instead of the overlay)
'invisible'The literal string 'invisible' — suppresses the highlight entirely (mainly for automated testing)

HighlightOverlay also owns the group-focus-highlight and reading-block-highlight rendering, and exposes static setters for globally re-theming every highlight in an application:

ts
import { HighlightOverlay } from 'scenerystack/scenery';
import { Color } from 'scenerystack/scenery';

// Re-theme every focus highlight application-wide, e.g. for a dark background.
HighlightOverlay.setInnerHighlightColor( new Color( 'yellow' ) );
Static methodEffect
setInnerHighlightColor( color ) / getInnerHighlightColor()Inner stroke color used by all subsequently drawn focus highlights
setOuterHilightColor( color ) / getOuterHighlightColor()Outer stroke color (note the source's method name is setOuterHilightColor, missing a "g")
setInnerGroupHighlightColor / setOuterGroupHighlightColorSame, for group focus highlights

Line width and dash scale automatically with pan/zoom and Node scale

HighlightOverlay recomputes each highlight's line width and dash pattern every time its TransformTracker reports a transform change, combining the Node's own scale with the inverse of the current pan/zoom matrix (HighlightPath.getCorrectiveScalingMatrix()). This is why a focus highlight looks the same relative thickness whether the Node is scaled up, scaled down, or the user has zoomed in with AnimatedPanZoomListener — you don't need to correct for either case yourself.