DEV Community

Ellis
Ellis

Posted on • Edited on

React down slider (dynamic height)

We will create a React "down slider" component. When we click a button, the slide content will appear/disappear (fully or partially) by sliding down/up. The slider will adjust to the content height using the triggers we specify. We can place the button inside or outside the slider content.

Image description

We will use: React, typescript, styled-components.
I used vite to create the app.

First we create a new component in DownSlider.tsx:

import React from "react";
import styled from "styled-components";

type DownSliderProps = {
  expanded: boolean;
  collapsedHeight: number;
  expandedHeight: number;
};

// height doesn't animate, but max-height does
const Container = styled.div<DownSliderProps>`
  position: relative;
  background-color: beige;
  overflow: hidden; // hide any overflowing content

  ${({ expanded, collapsedHeight, expandedHeight }) =>
    expanded
      ? `max-height: ${expandedHeight}px; transition: max-height 0.5s;`
      : `max-height: ${collapsedHeight}px; transition: max-height 0.5s;`}
`;

// ------------------------------------
// You can place the button inside or outside the content.
type Props = {
  expanded: boolean;
  collapsedHeight: number; // height of the button, if inside content
  expandedHeight: number; // height of expanded content
  children: React.ReactNode;
};

// ------------------------------------
const DownSlider = ({
  expanded,
  collapsedHeight,
  expandedHeight,
  children,
}: Props) => {
  return (
    <Container
      expanded={expanded}
      collapsedHeight={collapsedHeight}
      expandedHeight={expandedHeight}
    >
      {children}
    </Container>
  );
};

export default DownSlider;
Enter fullscreen mode Exit fullscreen mode

Then in TestPage.tsx, we add the slider, a button, some content, and we simulate some delayed data access:

import React, { useEffect, useRef, useState } from "react";
import styled from "styled-components";

import { DownSliderButton, DownSlider, Page } from "components";

const DownSliderContainer = styled.div`
  width: 700px;
  padding: 10px;
  border-radius: 10px;
  border: 1px solid forestgreen;
`;

const SlideTitle = styled.div`
  display: flex;
  justify-content: space-between;
  align-items: center;
  background-color: #eee;
  font-weight: bold;
`;

const SlideContent = styled.div`
  padding: 10px;
`;

// ------------------------------------
const TestPage = () => {
  const ref: any = useRef(null); // to measure content height

  const initialHeight = 0; // 0: slides at start, ~1000: no sliding

  const [updatedHeight, setUpdatedHeight] = useState(initialHeight);
  const [isExpanded, setExpanded] = useState(true);
  const [delayedData, setDelayedData] = useState("");

  useEffect(() => {
    setUpdatedHeight(ref.current?.clientHeight);
  }, [ref.current?.clientHeight, delayedData]); // update triggers

  useEffect(() => {
    setTimeout(() => {
      setDelayedData("This data arrives ~2s later.");
    }, 2000);
  }, []);

  // ------------------------------------
  return (
    ...
      <DownSliderContainer>
        <SlideTitle>
          <div>Slide title</div>
          <DownSliderButton isExpanded={isExpanded} setExpanded={setExpanded} />
        </SlideTitle>

        <DownSlider
          expanded={isExpanded}
          collapsedHeight={0}
          expandedHeight={updatedHeight}
        >
          <SlideContent ref={ref}>
            <div>Slide content</div>

            <div>
              <img
                src={"/src/assets/cairn-terrier.jpg"}
                alt="dog"
                height={454}
              />
            </div>

            <div>More data arriving soon.</div>

            <div>{delayedData}</div>
          </SlideContent>
        </DownSlider>
      </DownSliderContainer>
    ...
  );
};

export default TestPage;
Enter fullscreen mode Exit fullscreen mode

For completeness, here's DownSliderButton.tsx:

import React from "react";
import styled from "styled-components";

// @ts-ignore
import { Rotator } from "components";
// @ts-ignore
import { CircleArrowRightIcon } from "svg";

const Container = styled.div`
  cursor: pointer;
  color: var(--color-primary-lighter);

  & > div {
    width: 32px;
    height: 32px;
  }
`;

// ------------------------------------
type Props = {
  isExpanded: boolean;
  setExpanded: (isExpanded: boolean) => void;
};

// ------------------------------------
const DownSliderButton = ({ isExpanded, setExpanded }: Props) => {
  return (
    <Container role="button" onClick={() => setExpanded(!isExpanded)}>
      <Rotator rotated={isExpanded ? 90 : 180}>
        <CircleArrowRightIcon />
      </Rotator>
    </Container>
  );
};

export default DownSliderButton;
Enter fullscreen mode Exit fullscreen mode

And the Rotator rotates anything you place inside it:

import styled, { css } from "styled-components";

type Props = {
  rotated?: number;
};

const Rotator = styled.div<Props>`
  transform: rotate(0deg);
  transition: transform 0.5s ease-out;

  ${({ rotated }) =>
    rotated !== undefined &&
    css`
      transform: rotate(${rotated}deg);
      transition: transform 0.5s ease-out;
    `};
`;

export default Rotator;
Enter fullscreen mode Exit fullscreen mode

I declare this button component inside my "svg/index.js" file:

export { ReactComponent as CircleArrowRightIcon } from "svg/circle-arrow-right.svg";
Enter fullscreen mode Exit fullscreen mode

.. and it imports this svg:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentcolor">
  <path d="M256 0C114.6 0 0 114.6 0 256c0 141.4 114.6 256 256 256s256-114.6 256-256C512 114.6 397.4 0 256 0zM406.6 278.6l-103.1 103.1c-12.5 12.5-32.75 12.5-45.25 0s-12.5-32.75 0-45.25L306.8 288H128C110.3 288 96 273.7 96 256s14.31-32 32-32h178.8l-49.38-49.38c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0l103.1 103.1C414.6 241.3 416 251.1 416 256C416 260.9 414.6 270.7 406.6 278.6z"/>
</svg>
Enter fullscreen mode Exit fullscreen mode

Question/comments, let me know.

Top comments (0)