Skip to content

ScrollArea

Basic usage

To use the ScrollArea component, wrap your content with ScrollArea. It will create a couple of div wraps and handle the necessary calculations. You can set the height or width directly on the ScrollArea or somewhere higher in the hierarchy. max-height and max-width are also supported.

tsx
import { Box, ScrollArea } from '@semcore/ui/base-components';
import React from 'react';

let randomIndex = 1;
const stableRandom = () => {
  if (randomIndex > 20) randomIndex = 1;
  return Math.abs(Math.sin(Math.exp(Math.PI * randomIndex * Math.cos(100 - randomIndex++))));
};
function getRandomColor() {
  const letters = '0123456789ABCDEF';
  let color = '#';
  for (let i = 0; i < 6; i++) {
    color += letters[Math.floor(stableRandom() * 16)];
  }
  return color;
}

class Demo extends React.PureComponent {
  render() {
    return (
      <ScrollArea h={300}>
        {[...new Array(100)].map((_, index) => (
          <Box
            key={index}
            inline
            m={2}
            w={120}
            h={120}
            style={{ backgroundColor: getRandomColor() }}
          />
        ))}
      </ScrollArea>
    );
  }
}

export default () => <Demo />;

Synchronized scroll

tsx
import { Box, Flex, ScrollArea } from '@semcore/ui/base-components';
import React from 'react';

let randomIndex = 1;
const stableRandom = () => {
  if (randomIndex > 20) randomIndex = 1;
  return Math.abs(Math.sin(Math.exp(Math.PI * randomIndex * Math.cos(100 - randomIndex++))));
};
function getRandomColor() {
  const letters = '0123456789ABCDEF';
  let color = '#';
  for (let i = 0; i < 6; i++) {
    color += letters[Math.floor(stableRandom() * 16)];
  }
  return color;
}

class Demo extends React.PureComponent {
  controlled: HTMLDivElement | null = null;
  handleMainScroll = (e: React.MouseEvent<HTMLDivElement>) => {
    if (this.controlled) {
      this.controlled.scrollTop = e.currentTarget.scrollTop;
    }
  };

  componentDidMount() {
    if (this.controlled) {
      this.controlled.scrollTop = 0;
    }
  }

  render() {
    return (
      <Flex>
        <Box style={{ position: 'relative' }}>
          <h3 id='main-title'>Main ScrollArea</h3>
          <ScrollArea w={300} h={300}>
            <ScrollArea.Container
              role='group'
              aria-labelledby='main-title'
              onScroll={this.handleMainScroll}
            >
              {[...new Array(100)].map((_, index) => (
                <Box
                  key={index}
                  inline
                  m={2}
                  w={120}
                  h={120}
                  style={{ backgroundColor: getRandomColor() }}
                />
              ))}
            </ScrollArea.Container>
            <ScrollArea.Bar />
          </ScrollArea>
        </Box>

        <Box>
          <h3 id='control-title'>Controlled ScrollArea</h3>
          <ScrollArea w={300} h={300}>
            <ScrollArea.Container
              role='group'
              aria-labelledby='control-title'
              ref={(node: HTMLDivElement | null) => {
                this.controlled = node;
              }}
            >
              {[...new Array(100)].map((_, index) => (
                <Box
                  key={index}
                  inline
                  m={2}
                  w={120}
                  h={120}
                  style={{ backgroundColor: getRandomColor() }}
                />
              ))}
            </ScrollArea.Container>
            <ScrollArea.Bar />
          </ScrollArea>
        </Box>
      </Flex>
    );
  }
}

export default () => <Demo />;

Synchronized reverse scroll

tsx
import { Box, Flex, ScrollArea } from '@semcore/ui/base-components';
import React from 'react';

let randomIndex = 1;
const stableRandom = () => {
  if (randomIndex > 20) randomIndex = 1;
  return Math.abs(Math.sin(Math.exp(Math.PI * randomIndex * Math.cos(100 - randomIndex++))));
};
function getRandomColor() {
  const letters = '0123456789ABCDEF';
  let color = '#';
  for (let i = 0; i < 6; i++) {
    color += letters[Math.floor(stableRandom() * 16)];
  }
  return color;
}

class Demo extends React.PureComponent {
  mirror: HTMLDivElement | null = null;
  handleScrollMain = (e: React.MouseEvent<HTMLDivElement>) => {
    if (this.mirror) {
      this.mirror.scrollTop =
        this.mirror.scrollHeight - this.mirror.clientHeight - e.currentTarget.scrollTop;
    }
  };

  componentDidMount() {
    if (this.mirror) {
      this.mirror.scrollTop = this.mirror.scrollHeight - this.mirror.clientHeight;
    }
  }

  render() {
    return (
      <Flex>
        <Box style={{ position: 'relative' }}>
          <h3 id='main-reverse-title'>Main ScrollArea</h3>
          <ScrollArea w={300} h={300}>
            <ScrollArea.Container
              role='group'
              aria-labelledby='main-reverse-title'
              onScroll={this.handleScrollMain}
            >
              {[...new Array(100)].map((_, index) => (
                <Box
                  key={index}
                  inline
                  m={2}
                  w={120}
                  h={120}
                  style={{ backgroundColor: getRandomColor() }}
                />
              ))}
            </ScrollArea.Container>
            <ScrollArea.Bar />
          </ScrollArea>
        </Box>

        <Box>
          <h3 id='control-reverse-title'>Reversed ScrollArea</h3>
          <ScrollArea w={300} h={300}>
            <ScrollArea.Container
              role='group'
              aria-labelledby='control-reverse-title'
              ref={(node: HTMLDivElement | null) => {
                this.mirror = node;
              }}
            >
              <Flex flexWrap reverse>
                {[...new Array(100)].map((_, index) => (
                  <Box
                    key={index}
                    inline
                    m={2}
                    w={120}
                    h={120}
                    style={{ backgroundColor: getRandomColor() }}
                  />
                ))}
              </Flex>
            </ScrollArea.Container>
            <ScrollArea.Bar />
          </ScrollArea>
        </Box>
      </Flex>
    );
  }
}

export default () => <Demo />;

Dynamic virtual list

The dynamic virtual list is powered by React-virtualized.

tsx
import { Box, Flex, ScrollArea } from '@semcore/ui/base-components';
import Button from '@semcore/ui/button';
import { Text } from '@semcore/ui/typography';
import React from 'react';
import { List } from 'react-virtualized';

const list = [...new Array(6)];
const renderRow = ({
  key,
  index,
  style,
}: { key: string; index: number; style: React.CSSProperties }) => {
  return (
    <Box
      key={key}
      inline
      m={2}
      w={120}
      h={120}
      style={{ border: '1px solid black', ...style }}
      role='row'
    >
      <Text bold size={200} m='auto' role='gridcell'>
        {index + 1}
      </Text>
    </Box>
  );
};

const Demo = () => {
  const [data, setData] = React.useState(list);
  const innerRef: React.MutableRefObject<HTMLDivElement | null> = React.useRef(null);
  const ref = (node: HTMLDivElement | null) => {
    if (node && innerRef.current) {
      innerRef.current = node.querySelector('.ReactVirtualized__Grid__innerScrollContainer');
    }
  };

  return (
    <Flex direction='column' inline>
      <Flex alignItems='center' mb={2} gap={2}>
        <Button
          onClick={() => {
            setData(data.concat(undefined));
          }}
        >
          Add item
        </Button>
        <Button onClick={() => setData(data.slice(0, -1))}>Remove item</Button>
        <Text role='status' aria-live='polite'>
          Count:
          {' '}
          {data.length}
        </Text>
      </Flex>
      <Box h={500}>
        {data.length
        // eslint-disable-next-line @stylistic/multiline-ternary
          ? (
              <ScrollArea inner={innerRef}>
                <ScrollArea.Container
                  ref={ref}
                  // @ts-ignore
                  tag={List}
                  height={500}
                  rowCount={data.length}
                  width={500}
                  rowHeight={120}
                  rowRenderer={renderRow}
                />
                <ScrollArea.Bar orientation='vertical' />
              </ScrollArea>
            ) : null}
      </Box>
    </Flex>
  );
};

export default Demo;

Released under the MIT License.

Released under the MIT License.