ScrollBoundaryContainer is used with anchor-based components such as Popover, Tooltip, Dropdown or ComboBox. ScrollBoundaryContainer is needed for proper positioning when the anchor-based component is anchored to an element that is located within a scrolling container. The use of ScrollBoundaryContainer ensures the anchor-based component remains attached to its anchor when scrolling. Don't use ScrollBoundaryContainer to add scrolling to a container, use Box's props instead.

also known as ScrollableContainer

Figma:

Responsive:

Adaptive:

Props

Component props
Name
Type
Default
children
Required
React.Node
-
height
number | string
"100%"

Use numbers for pixels: height={100} and strings for percentages: height="100%".

Overflow property only works for elements with a specified height. It is not required if the parent component sets the height. See the height variant for more details.

overflow
"scroll" | "scrollX" | "scrollY" | "auto" | "visible"
"auto"

Accessibility

Variants

Height

When scrolling is desired, we must explicitly set a height. Unless a height is set, the content will push the parent container's height.

In ScrollBoundaryContainer, height is an optional prop with a default value of 100%. If ScrollBoundaryContainer’s immediate parent is a component with a fixed height, do not pass a height to ScrollBoundaryContainer as seen in first example below. On the other hand, if there isn’t an immediate parent fixing the height, you must specify the ScrollBoundaryContainer height as seen in the second example below.

import { useEffect, useRef, useState } from 'react';
import {
  Box,
  ButtonLink,
  Flex,
  PopoverEducational,
  ScrollBoundaryContainer,
  Text,
} from 'gestalt';

export default function Example() {
  const [open, setOpen] = useState(false);
  const anchorRef = useRef(null);

  useEffect(() => {
    setOpen(true);
  }, []);

  return (
    <Box color="secondary" height="100%" padding={4}>
      <ScrollBoundaryContainer height={200}>
        <Box color="light" padding={4} width={600}>
          <Flex gap={{ column: 0, row: 4 }}>
            <Box width={200}>
              <Text>
                You need to add your data source URL to Pinterest so we can
                access your data source file and create Pins for your products.
                Before you do this, make sure you have prepared your data source
                and that you have claimed your website. If there are any errors
                with your data source file, you can learn how to troubleshoot
                them below. After you click Create Pins, you will land back at
                the main data source page while your feed is being processed.
                Wait for a confirmation email from Pinterest about the status of
                your data source submission.
              </Text>
            </Box>
            <ButtonLink
              ref={anchorRef}
              href="https://help.pinterest.com/en/business/article/data-source-ingestion"
              iconEnd="visit"
              onClick={() => setOpen(false)}
              target="blank"
              text="Help"
            />
            {open && (
              <PopoverEducational
                anchor={anchorRef.current}
                idealDirection="right"
                message="Need help with something? Check out our Help Center."
                onDismiss={() => {}}
              />
            )}
          </Flex>
        </Box>
      </ScrollBoundaryContainer>
    </Box>
  );
}

Overflow

In most cases, overflow set to "auto" is the expected behavior for most containers. If the scrolling container is large enough, Popover-based components are displayed without problems.

However, if ScrollBoundaryContainer is small and hides Popover-based components within its boundaries, overflow set to "visible" displays Popover-based components above ScrollBoundaryContainer. This approach should only be used when there is no chance that the content within ScrollBoundaryContainer can overflow outside of it and, therefore, require scroll.

Use this approach if implementing ScrollBoundaryContainer in a higher parent container doesn't fix your Popover-based components positioning issues as expected.

See the examples below to compare the implementation in both circumnstances.

Overflow="auto"
import { useState } from 'react';
import {
  Box,
  Flex,
  Icon,
  RadioGroup,
  ScrollBoundaryContainer,
  TapArea,
  Text,
  Tooltip,
} from 'gestalt';

export default function Example() {
  const [content, setContent] = useState(null);
  const [claimed, setClaimed] = useState(null);
  const [device, setDevice] = useState(null);

  return (
    <Box color="secondary" height="100%" padding={4}>
      <ScrollBoundaryContainer height={200} overflow="auto">
        <Box color="light" padding={3}>
          <Flex direction="column" gap={{ column: 4, row: 0 }} width={300}>
            <Flex direction="column" gap={{ column: 2, row: 0 }}>
              <Flex alignItems="center" gap={{ row: 1, column: 0 }}>
                <Text weight="bold">Content type</Text>
                <Tooltip
                  idealDirection="right"
                  text="See stats about different types of content created by you and/or others on Pinterest. Filter to get more details on your organic (not an ad) and paid (promoted as an ad) content."
                >
                  <TapArea>
                    <Icon
                      accessibilityLabel="Information"
                      color="default"
                      icon="info-circle"
                    />
                  </TapArea>
                </Tooltip>
              </Flex>
              <Flex direction="column">
                <RadioGroup
                  id="content-type"
                  legend="Select content type"
                  legendDisplay="hidden"
                >
                  <RadioGroup.RadioButton
                    checked={content === 'all'}
                    id="allcontent"
                    label="All"
                    name="content"
                    onChange={() => setContent('all')}
                    value="all"
                  />
                  <RadioGroup.RadioButton
                    checked={content === 'organic'}
                    id="organic"
                    label="Organic"
                    name="content"
                    onChange={() => setContent('organic')}
                    value="organic"
                  />
                  <RadioGroup.RadioButton
                    checked={content === 'paid'}
                    id="paid"
                    label="Paid and earned"
                    name="content"
                    onChange={() => setContent('paid')}
                    value="paid"
                  />
                </RadioGroup>
              </Flex>
            </Flex>
            <Flex direction="column" gap={{ column: 2, row: 0 }}>
              <Flex alignItems="center" gap={{ row: 1, column: 0 }}>
                <Text weight="bold">Claimed account</Text>
                <Tooltip
                  idealDirection="right"
                  text="See stats for Pins linked to your claimed accounts like websites, Etsy, Instagram or Youtube. The Other Pins category includes Pins you’ve published or saved that don’t link to any of your claimed accounts."
                >
                  <TapArea>
                    <Icon
                      accessibilityLabel="Information"
                      color="default"
                      icon="info-circle"
                    />
                  </TapArea>
                </Tooltip>
              </Flex>
              <Flex direction="column">
                <RadioGroup
                  id="claimed-account"
                  legend="Select claimed account"
                  legendDisplay="hidden"
                >
                  <RadioGroup.RadioButton
                    checked={claimed === 'all'}
                    id="allclaimed"
                    label="All Pins"
                    name="claimed"
                    onChange={() => setClaimed('all')}
                    value="all"
                  />
                  <RadioGroup.RadioButton
                    checked={claimed === 'instagram'}
                    id="instagram"
                    label="Instagram"
                    name="claimed"
                    onChange={() => setClaimed('instagram')}
                    value="instagram"
                  />
                  <RadioGroup.RadioButton
                    checked={claimed === 'other'}
                    id="other"
                    label="Other pins"
                    name="claimed"
                    onChange={() => setClaimed('other')}
                    value="other"
                  />
                </RadioGroup>
              </Flex>
            </Flex>
            <Flex direction="column" gap={{ column: 2, row: 0 }}>
              <Flex alignItems="center" gap={{ row: 1, column: 0 }}>
                <Text weight="bold">Device</Text>
                <Tooltip
                  idealDirection="right"
                  text="See stats for the different devices your Pinterest traffic is coming from."
                >
                  <TapArea>
                    <Icon
                      accessibilityLabel="Information"
                      color="default"
                      icon="info-circle"
                    />
                  </TapArea>
                </Tooltip>
              </Flex>
              <Flex direction="column">
                <RadioGroup
                  id="device-type"
                  legend="Select a device"
                  legendDisplay="hidden"
                >
                  <RadioGroup.RadioButton
                    checked={device === 'all'}
                    id="alldevices"
                    label="All"
                    name="device"
                    onChange={() => setDevice('all')}
                    value="all"
                  />
                  <RadioGroup.RadioButton
                    checked={device === 'mobile'}
                    id="mobile"
                    label="Mobile"
                    name="device"
                    onChange={() => setDevice('mobile')}
                    value="mobile"
                  />
                  <RadioGroup.RadioButton
                    checked={device === 'desktop'}
                    id="desktop"
                    label="Desktop"
                    name="device"
                    onChange={() => setDevice('desktop')}
                    value="desktop"
                  />
                  <RadioGroup.RadioButton
                    checked={device === 'tablet'}
                    id="tablet"
                    label="Tablet"
                    name="device"
                    onChange={() => setDevice('tablet')}
                    value="tablet"
                  />
                </RadioGroup>
              </Flex>
            </Flex>
          </Flex>
        </Box>
      </ScrollBoundaryContainer>
    </Box>
  );
}

Overflow="visible"
import { useState } from 'react';
import {
  Box,
  Flex,
  Icon,
  RadioGroup,
  ScrollBoundaryContainer,
  TapArea,
  Text,
  Tooltip,
} from 'gestalt';

export default function Example() {
  const [content, setContent] = useState(null);
  const [claimed, setClaimed] = useState(null);
  const [device, setDevice] = useState(null);

  return (
    <Box color="secondary" height="100%" padding={4}>
      <Box color="light" height={200} overflow="auto" padding={3}>
        <Flex direction="column" gap={{ column: 4, row: 0 }} width={300}>
          <ScrollBoundaryContainer overflow="visible">
            <Flex direction="column" gap={{ column: 2, row: 0 }}>
              <Flex alignItems="center" gap={{ row: 1, column: 0 }}>
                <Text weight="bold">Content type</Text>
                <Tooltip
                  idealDirection="right"
                  text="See stats about different types of content created by you and/or others on Pinterest. Filter to get more details on your organic (not an ad) and paid (promoted as an ad) content."
                >
                  <TapArea>
                    <Icon
                      accessibilityLabel="Information"
                      color="default"
                      icon="info-circle"
                    />
                  </TapArea>
                </Tooltip>
              </Flex>
              <Flex direction="column">
                <RadioGroup
                  id="content-type"
                  legend="Select content type"
                  legendDisplay="hidden"
                >
                  <RadioGroup.RadioButton
                    checked={content === 'all'}
                    id="allcontent"
                    label="All"
                    name="content"
                    onChange={() => setContent('all')}
                    value="all"
                  />
                  <RadioGroup.RadioButton
                    checked={content === 'organic'}
                    id="organic"
                    label="Organic"
                    name="content"
                    onChange={() => setContent('organic')}
                    value="organic"
                  />
                  <RadioGroup.RadioButton
                    checked={content === 'paid'}
                    id="paid"
                    label="Paid and earned"
                    name="content"
                    onChange={() => setContent('paid')}
                    value="paid"
                  />
                </RadioGroup>
              </Flex>
            </Flex>
          </ScrollBoundaryContainer>

          <ScrollBoundaryContainer overflow="visible">
            <Flex direction="column" gap={{ column: 2, row: 0 }}>
              <Flex alignItems="center" gap={{ row: 1, column: 0 }}>
                <Text weight="bold">Claimed account</Text>
                <Tooltip
                  idealDirection="right"
                  text="See stats for Pins linked to your claimed accounts like websites, Etsy, Instagram or Youtube. The Other Pins category includes Pins you’ve published or saved that don’t link to any of your claimed accounts."
                >
                  <TapArea>
                    <Icon
                      accessibilityLabel="Information"
                      color="default"
                      icon="info-circle"
                    />
                  </TapArea>
                </Tooltip>
              </Flex>
              <Flex direction="column">
                <RadioGroup
                  id="claimed-account"
                  legend="Select claimed account"
                  legendDisplay="hidden"
                >
                  <RadioGroup.RadioButton
                    checked={claimed === 'all'}
                    id="allclaimed"
                    label="All Pins"
                    name="claimed"
                    onChange={() => setClaimed('all')}
                    value="all"
                  />
                  <RadioGroup.RadioButton
                    checked={claimed === 'instagram'}
                    id="instagram"
                    label="Instagram"
                    name="claimed"
                    onChange={() => setClaimed('instagram')}
                    value="instagram"
                  />
                  <RadioGroup.RadioButton
                    checked={claimed === 'other'}
                    id="other"
                    label="Other pins"
                    name="claimed"
                    onChange={() => setClaimed('other')}
                    value="other"
                  />
                </RadioGroup>
              </Flex>
            </Flex>
          </ScrollBoundaryContainer>

          <ScrollBoundaryContainer overflow="visible">
            <Flex direction="column" gap={{ column: 2, row: 0 }}>
              <Flex alignItems="center" gap={{ row: 1, column: 0 }}>
                <Text weight="bold">Device</Text>
                <Tooltip
                  idealDirection="right"
                  text="See stats for the different devices your Pinterest traffic is coming from."
                >
                  <TapArea>
                    <Icon
                      accessibilityLabel="Information"
                      color="default"
                      icon="info-circle"
                    />
                  </TapArea>
                </Tooltip>
              </Flex>
              <Flex direction="column">
                <RadioGroup
                  id="device-type"
                  legend="Select a device"
                  legendDisplay="hidden"
                >
                  <RadioGroup.RadioButton
                    checked={device === 'all'}
                    id="alldevices"
                    label="All"
                    name="device"
                    onChange={() => setDevice('all')}
                    value="all"
                  />
                  <RadioGroup.RadioButton
                    checked={device === 'mobile'}
                    id="mobile"
                    label="Mobile"
                    name="device"
                    onChange={() => setDevice('mobile')}
                    value="mobile"
                  />
                  <RadioGroup.RadioButton
                    checked={device === 'desktop'}
                    id="desktop"
                    label="Desktop"
                    name="device"
                    onChange={() => setDevice('desktop')}
                    value="desktop"
                  />
                  <RadioGroup.RadioButton
                    checked={device === 'tablet'}
                    id="tablet"
                    label="Tablet"
                    name="device"
                    onChange={() => setDevice('tablet')}
                    value="tablet"
                  />
                </RadioGroup>
              </Flex>
            </Flex>
          </ScrollBoundaryContainer>
        </Flex>
      </Box>
    </Box>
  );
}

Built-in component

Modal and OverlayPanel come with ScrollBoundaryContainer built-in, so any anchored components used in their children tree should work out-of-the-box. Passing an additional ScrollBoundaryContainer will break the existing styling on scroll.

The following example shows the internal ScrollBoundaryContainer in action. The main content of both Modal and OverlayPanel is a form which includes Dropdown and ComboBox.

import { useRef, useState } from 'react';
import {
  Box,
  Button,
  ComboBox,
  CompositeZIndex,
  Dropdown,
  FixedZIndex,
  Flex,
  Heading,
  Layer,
  Modal,
  OverlayPanel,
  RadioGroup,
  TextField,
} from 'gestalt';

export default function ScrollBoundaryContainerExample() {
  const [showComponent, setShowComponent] = useState(false);
  const [open, setOpen] = useState(false);
  const [selected, setSelected] = useState(null);
  const [parentComponent, setParentComponent] = useState('modal');
  const anchorDropdownRef = useRef(null);

  const handleSelect = ({ item }) => setSelected(item);

  const MODAL_Z_INDEX = new FixedZIndex(11);

  const ANCHORED_Z_INDEX = new CompositeZIndex([MODAL_Z_INDEX]);

  const footer = (
    <Box flex="grow" paddingX={3} paddingY={3}>
      <Box
        display="flex"
        justifyContent="end"
        marginBottom={-1}
        marginEnd={-1}
        marginStart={-1}
        marginTop={-1}
        wrap
      >
        <Box paddingX={1} paddingY={1}>
          <Button
            onClick={() => setShowComponent(false)}
            size="lg"
            text="Cancel"
          />
        </Box>
        <Box paddingX={1} paddingY={1}>
          <Button
            color="red"
            onClick={() => setShowComponent(false)}
            size="lg"
            text="Save"
            type="submit"
          />
        </Box>
      </Box>
    </Box>
  );

  const children = (
    <Flex justifyContent="center">
      <Box
        direction="column"
        display="flex"
        marginBottom={-3}
        marginEnd={-3}
        marginStart={-3}
        marginTop={-3}
        maxWidth={800}
        width="100%"
        wrap
      >
        <Box display="flex" justifyContent="start" padding={3}>
          <Button
            ref={anchorDropdownRef}
            accessibilityControls="subtext-dropdown-example"
            accessibilityExpanded={open}
            accessibilityHaspopup
            accessibilityLabel="Select Previous Address"
            iconEnd="arrow-down"
            onClick={() => setOpen((prevVal) => !prevVal)}
            selected={open}
            text="Select Previous Address"
          />
          {open && (
            <Dropdown
              anchor={anchorDropdownRef.current}
              id="subtext-dropdown-example"
              onDismiss={() => {
                setOpen(false);
              }}
              zIndex={ANCHORED_Z_INDEX}
            >
              <Dropdown.Item
                onSelect={handleSelect}
                option={{
                  value: 'Headquarters San Francisco',
                  label: 'Headquarters San Francisco',
                  subtext: '321 Inspiration Street, Suite # 12',
                }}
                selected={selected}
              />
              <Dropdown.Item
                onSelect={handleSelect}
                option={{
                  value: 'Headquarters Seattle',
                  label: 'Headquarters Seattle',
                  subtext: '123 Creativity Street, Suite # 21',
                }}
                selected={selected}
              />
            </Dropdown>
          )}
        </Box>
        <Box flex="grow" paddingX={3} paddingY={3}>
          <Heading accessibilityLevel={2} size="400">
            Billing Address
          </Heading>
        </Box>
        <Box flex="grow" paddingX={3} paddingY={3}>
          <TextField
            id="Address_Name"
            label="Address Name"
            onChange={() => {}}
          />
        </Box>
        <Box flex="grow" paddingX={3} paddingY={3}>
          <TextField
            id="Business_Name"
            label="Business Name"
            onChange={() => {}}
          />
        </Box>
        <Box flex="grow" paddingX={3} paddingY={3}>
          <TextField
            id="Address_Line_1"
            label="Address Line 1"
            onChange={() => {}}
          />
        </Box>
        <Box flex="grow" paddingX={3} paddingY={3}>
          <TextField
            id="Address_Line_2"
            label="Address Line 2"
            onChange={() => {}}
          />
        </Box>
        <Box flex="grow" paddingX={3} paddingY={3}>
          <Box
            display="flex"
            marginBottom={-3}
            marginEnd={-3}
            marginStart={-3}
            marginTop={-3}
            wrap
          >
            <Box flex="grow" paddingX={3} paddingY={3}>
              <TextField id="City" label="City" onChange={() => {}} />
            </Box>
            <Box flex="grow" paddingX={3} paddingY={3}>
              <TextField
                id="State_Province_Region"
                label="State/Province/Region"
                onChange={() => {}}
              />
            </Box>
          </Box>
        </Box>
      </Box>
      <Box flex="grow" paddingX={3} paddingY={3}>
        <ComboBox
          accessibilityClearButtonLabel="Clear countries"
          id="Country"
          inputValue="United States"
          label="Country"
          noResultText="No Results"
          onChange={() => {}}
          onSelect={() => {}}
          options={[
            {
              value: 'United States',
              label: 'United States',
            },
            {
              value: 'Canada',
              label: 'Canada',
            },
            {
              value: 'United Kingdom',
              label: 'United Kingdom',
            },
            {
              value: 'Brazil',
              label: 'Brazil',
            },
            {
              value: 'Japan',
              label: 'Japan',
            },
          ]}
          placeholder="Select a Country"
        />
      </Box>
    </Flex>
  );

  return (
    <Box height="100%" padding={4}>
      <RadioGroup id="example" legend="Select Modal or OverlayPanel">
        <RadioGroup.RadioButton
          checked={parentComponent === 'modal'}
          id="modal"
          label="Open Modal"
          name="parentComponent"
          onChange={() => setParentComponent('modal')}
          value="modal"
        />
        <RadioGroup.RadioButton
          checked={parentComponent === 'overlaypanel'}
          id="overlaypanel"
          label="Open OverlayPanel"
          name="parentComponent"
          onChange={() => setParentComponent('overlaypanel')}
          value="overlaypanel"
        />
        <Button
          onClick={() => setShowComponent(true)}
          text="Update Billing Address"
        />
      </RadioGroup>
      {showComponent && (
        <Layer zIndex={MODAL_Z_INDEX}>
          {parentComponent === 'modal' ? (
            <Modal
              accessibilityModalLabel=""
              footer={footer}
              heading="Billing Information"
              onDismiss={() => setShowComponent(false)}
              size="lg"
            >
              {children}
            </Modal>
          ) : (
            <OverlayPanel
              accessibilityDismissButtonLabel="Dismiss Billing Information OverlayPanel"
              accessibilityLabel=""
              footer={footer}
              heading="Billing Information"
              onDismiss={() => setShowComponent(false)}
              size="lg"
            >
              {children}
            </OverlayPanel>
          )}
        </Layer>
      )}
    </Box>
  );
}

Component quality checklist

Component quality checklist
Quality item
Status
Status description
Figma Library
Component is not currently available in Figma.
Responsive Web
Ready
Component responds to changing viewport sizes in web and mobile web.

Box
Box's overflow prop specifies what should happen if the content is larger than the bounding box. Box should not be replaced with ScrollBoundaryContainer if the goal is simply to allow Box to scroll when content overflows. ScrollBoundaryContainer is only needed when anchored components, such as Tooltip, Popover, ComboBox or Dropdown, are used within a container that could potentially scroll.

Modal / OverlayPanel
Modal and OverlayPanel come with ScrollBoundaryContainer built-in, so any anchored components used in their children tree should work out-of-the-box. Passing an additional ScrollBoundaryContainer will break the existing styling on scroll.

Tooltip / Popover / Dropdown / ** ComboBox ** **
ScrollBoundaryContainer must be used around any of these components if they are used within a container that could possibly scroll. This is necessary to ensure the component remains attached to its anchor on scroll. If they are located within scrolling Modal or OverlayPanel components, ScrollBoundaryContainer isn't needed as it's already built-in.