Skip to content

A Resizable/Collapsible Panel ​

Task: a group of controls (a settings cluster, a "graph" section) should be collapsible down to just a title bar, reclaiming screen space, while still being bordered and styled consistently with the rest of the sim's non-collapsible panels.

AccordionBox is the collapsible panel — it already draws its own bordered background (styled the same way Panel is) and handles the expand/collapse interaction and layout resizing. You reach for a plain Panel only for a group that's never collapsible; reach for AccordionBox directly when collapsibility itself is the requirement, rather than trying to wrap a Panel in some separate show/hide mechanism.

The solution ​

ts
import { AccordionBox } from 'scenerystack/sun';
import { VBox, Text } from 'scenerystack/scenery';
import { Checkbox } from 'scenerystack/sun';
import { BooleanProperty } from 'scenerystack/axon';

// --- model-owned state the controls act on ---
const gravityEnabledProperty = new BooleanProperty( true );
const frictionEnabledProperty = new BooleanProperty( false );

// --- the collapsible state itself is also a Property, owned externally so
// it can be read/reset alongside the rest of the screen's state ---
const isControlsExpandedProperty = new BooleanProperty( true );

const controlsContent = new VBox( {
  spacing: 8,
  align: 'left',
  children: [
    new Checkbox( gravityEnabledProperty, new Text( 'Gravity' ) ),
    new Checkbox( frictionEnabledProperty, new Text( 'Friction' ) )
  ]
} );

const controlsAccordionBox = new AccordionBox( controlsContent, {
  titleNode: new Text( 'Controls' ),
  expandedProperty: isControlsExpandedProperty,
  cornerRadius: 8,
  titleAlignX: 'left'
} );

Clicking the title bar (or its expand/collapse button) toggles isControlsExpandedProperty, which collapses controlsAccordionBox down to just the title row — controlsContent's children are hidden, not destroyed, so re-expanding shows the same live controls with no rebuilding.

Resetting the collapsed state with the rest of the screen ​

Because isControlsExpandedProperty is supplied externally (rather than using expandedDefaultValue), it needs to be reset explicitly alongside the model's own Properties — AccordionBox.reset() is a no-op for externally-supplied Properties:

ts
class MyScreenModel {
  public reset(): void {
    gravityEnabledProperty.reset();
    frictionEnabledProperty.reset();
  }
}

// in the ResetAllButton's listener (see The Reset-All Pattern):
resetAllButton = new ResetAllButton( {
  listener: () => {
    model.reset();
    isControlsExpandedProperty.reset(); // view-owned, but still reset with everything else
  }
} );

If you don't need to observe or reset the expanded state from outside, skip the external Property entirely and just pass expandedDefaultValue: false — AccordionBox will create and own its own expandedProperty, and controlsAccordionBox.reset() resets it for you.

Options used here ​

OptionEffect
titleNodeNode shown in the always-visible title bar
expandedPropertyExternally-owned Property<boolean> driving expanded/collapsed state
expandedDefaultValueAlternative to expandedProperty — lets AccordionBox own and reset its own Property
cornerRadius / titleAlignXVisual styling, matching the conventions of a plain Panel

Collapsed width defaults to matching the expanded width

useExpandedBoundsWhenCollapsed defaults to true, so the collapsed title bar stays exactly as wide as the box is when expanded — this keeps a column of accordion boxes (or an accordion box next to a fixed-width sibling panel) visually aligned regardless of expanded state, instead of the collapsed row shrinking to just the title text's width.

Don't nest a Panel inside an AccordionBox's content expecting a second border

AccordionBox already draws the same kind of bordered background Panel does — wrapping controlsContent in its own Panel before passing it to AccordionBox just produces a border-within-a-border. Pass the bare content (a VBox/GridBox of controls) directly, as shown above.