Skip to main content
Kaleidoscope

Layering

Provides consistent and automatic layering of overlays and components that use stacking in our application.

Background

Using the layering components here allows you to build overlay components (components that float over the top of other components) that are aware of their layering context, and automatically stack correctly.

This is important because:

  • we want to be able to create components that exist inside portals, but stack correctly on top of their parents
  • components may be used inside modals, and we want to ensure that they can stack over the top of the modal
  • we want to avoid having to manually specify z-indices in components, which increases reusability

Kaleidoscope support

Auto-stacking is available for many Kaleidoscope components that have overlays:

  • Toolbar
  • Tooltip
  • Popover
  • OptionMenu
  • Select

They automatically infer the z-index that it is currently rendered in, and set the z-indices appropriately. Set the autoStack prop to false to opt-out of this behaviour.

Example usage

Below is a complex example of several Kaleidoscope components that stack automatically over the top of each other.

Note the use of the autoStack prop set on each of the stacked components.

Editable example
() => {
  const [inputFocussed, setInputFocussed] = React.useState(false);

  const [variableMenuOpen, setVariableMenuOpen] = React.useState(false);

  const textInputRef = React.useRef();

  return (
    <Popover
      button={(buttonProps) => (
        <Button type="secondary" {...buttonProps}>
          Edit ROI formula
        </Button>
      )}
    >
      <h2 className="text-h4">Configure the ROI calculator</h2>
      <Toolbar
        visible={inputFocussed || variableMenuOpen}
        element={textInputRef.current}
        positionType={ToolbarPositionType.Element}
      >
        <OptionMenu button={
          <ToolbarButton
            icon={<Variables />}
            tooltip={{content: "Add variables"}}
            onClick={() => {
              setVariableMenuOpen(!variableMenuOpen)
            }}
          />
        }>
          {["A", "B", "C"].map(v => (
          <OptionMenuItem
            onClick={() => {
              setVariableMenuOpen(false);
            }}
            key={v}
          >
            Variable {v}
          </OptionMenuItem>
          ))}
        </OptionMenu>
      </Toolbar>
      <TextInput
        label="Formula"
        multiline
        onFocus={() => setInputFocussed(true)}
        onBlur={() => setInputFocussed(false)}
        ref={textInputRef}
      />
    </Popover>
  )
}

APIs

There are two core APIs we currently have to add functionality beyond autoStack on individual components.

  • Layer - for creating new autostacking components, and setting manual layers
  • AngularOverlayProvider - for creating a layer that's compatible with our Angular overlay pattern

These use React's Context feature under the hood to store z-indices, but we intentionally do not export the underlying context.

Layer

The Layer component can be used to:

  • create new components that stack above the current component (this is what Kaleidoscope components use under the hood to auto-stack)
  • shim onto an existing layout by setting the offset prop, for when you are working with existing layers or need to set z-indices for some components manually

Creating a new component that stacks

A good example of a component that utilises the Layer under the hood to auto-apply a z-index is the component.

Example usage of Layer to shim across legacy z-indices

Here, we have a blue rectangle with a manually set z-index, and a toolbar that is rendered inside of it.

Without autostacking of the toolbar and a Layer with an offset equivalent to the z-index of the blue rectangle, it would be behind the blue rectangle.

Editable example
() => {
  const paragraphRef = React.useRef();
  const [showToolbar, setShowToolbar] = React.useState(false);
  const [autostackToolbar, setAutostackToolbar] = React.useState(true);
  return (
    <>
      <div style={{position: "absolute", zIndex: 5, background: "lightblue", height: "150px", width: "250px", padding: "var(--space-m)"}}>
        <p style={{margin: "var(--space-m) 0"}} ref={paragraphRef}>Manually positioned layer</p>
        <Button onClick={() => setShowToolbar(true)}>Show toolbar</Button>
        <Checkbox
            id="abc123"
            label="Autostack toolbar"
            checked={autostackToolbar}
            onChange={() => setAutostackToolbar(!autostackToolbar)}
         />
        <Layer offset={5}>
          {() => (
            <Toolbar visible={showToolbar} positionType={ToolbarPositionType.Element} element={paragraphRef.current} autoStack={autostackToolbar}>
                <ToolbarButton
                  icon={<Variables />}
                  tooltip={{content: "Add variables"}}
                  onClick={() => {
                    setVariableMenuOpen(!variableMenuOpen)
                  }}
                />
            </Toolbar>
          )}
        </Layer>
      </div>

      // Spacer div
      <div style={{height: "200px"}}></div>
    </>
  )
}

Note that offset is an additive value. For example, rendering a Layer with an offset of 5 inside another Layer with an offset of 6 will add to a z-index 11 on the top Layer

Common antipattern: wrapping individual components with Layer

A common mistake is to wrap individual components deep in the tree with <Layer>, rather than placing a single <Layer> at the root of the React mount that knows about its z-index context.

Don't do this

// ❌ Wrapping individual components with Layer deep in the tree
const FieldOptionsMenu = ({ onConfigure, onDelete }) => {
return (
<Layer offset={sidebarZIndex}>
{({ zIndex }) => (
<OptionMenu style={{ zIndex }}>
<OptionMenuItem onClick={onConfigure}>Configure</OptionMenuItem>
<OptionMenuItem onClick={onDelete}>Delete</OptionMenuItem>
</OptionMenu>
)}
</Layer>
);
};

This is problematic because:

  • Every component that uses overlays now needs to know the z-index of its parent container
  • It's easy to forget, leading to inconsistent stacking
  • It bypasses the automatic stacking that Kaleidoscope components already provide via autoStack

Do this instead

Place a single <Layer> at the root of the React mount that "knows" about its stacking context (e.g. a sidebar, modal, or panel). All child components with autoStack (which is the default) will then stack correctly for free.

// ✅ Single Layer at the root of the mount
const TemplateSettingsSidebar = () => {
return (
<Layer offset={sidebarZIndex}>
{() => (
<div className={styles.sidebar}>
{/* These components autoStack correctly without any extra work */}
<FieldOptionsMenu onConfigure={handleConfigure} onDelete={handleDelete} />
<Tooltip content="Help text">...</Tooltip>
</div>
)}
</Layer>
);
};
// The child component doesn't need to know about layers at all
const FieldOptionsMenu = ({ onConfigure, onDelete }) => {
return (
<OptionMenu>
<OptionMenuItem onClick={onConfigure}>Configure</OptionMenuItem>
<OptionMenuItem onClick={onDelete}>Delete</OptionMenuItem>
</OptionMenu>
);
};

The key insight is that Layer sets the stacking context for all descendants via React Context. Components like OptionMenu, Tooltip, Popover, Toolbar, and Select read this context automatically when autoStack is enabled (the default). You only need Layer at the boundary where your React tree meets an external z-index (e.g. an Angular overlay, a sidebar with a manual z-index).

AngularOverlayProvider

The AngularOverlayProvider component can wrap the root of a component tree that exists inside an Angular modal, to ensure any components using the layering context know that they should be placed above the Angular modal.

This is useful for ensuring Kaleidoscope components work in parts of our app that are using Angular modals, such as the saved block editor.