How to Prevent Layout Shifts and Whitespace Increasing when Conditionally Rendering Content in Next.js

I’m creating a page in NextJS (App Router) that is split into 2, separately scrollable sides.

One of my sides features toggles, that conditionally add / remove content from it.

When the toggles are switched, increasing the height of the side, for some reason, whitespace is appearing at the bottom of the DOM, proportional to the size of the rendered component.

How can I prevent this whitespace from appearing, and instead enable the height of the side to dynamically adjust to the content within it?

Below is a minimal reproduction of how I’ve currently got my code set up.

Here is a minimal version of the main page.tsx component. It is structured like so (92vh as navbar is 8)

"use client";
import React, { useState } from "react";
import SizeInput from "./SizeInput";

const Home = () => {
  const [asset, setAsset] = useState({
    symbol: "BTC",
    price: 20000,
    leverage: 10,
    image: "/img/trade/BTC-Logo.png",
  });

  return (
    <div className="flex flex-col xl:h-[92vh] overflow-hidden pb-14 lg:pb-0">
      <div className="flex flex-col xl:flex-row w-full h-full bg-[#07080A]">
        <div className="flex flex-col justify-between xl:w-[70%] xl:h-full xl:overflow-y-auto no-scrollbar">
          <div className="p-4">
            <p>Main content here</p>
            {[...Array(50)].map((_, i) => (
              <p key={i}>This is line {i + 1} of the main content</p>
            ))}
          </div>
        </div>
        <div className="flex flex-col justify-start xl:w-[30%] xl:h-full xl:overflow-y-auto no-scrollbar">
          <div className="flex flex-col w-full gap-4 bg-card-grad border-cardborder border-2 p-4">
            <div className="h-[600px]">
              {"Div to take up space so heigh > 100vh"}
            </div>
            <SizeInput isLong={true} activeType="Market" />
          </div>
        </div>
      </div>
    </div>
  );
};

export default Home;

Within the 30% side of the page I have a SizeInput component, which contains toggles, that conditionally render a couple of components.

Here is a minimal version of the SizeInput component

"use client";
import React, { useEffect, useState } from "react";
import Image from "next/image";
import ToggleSwitch from "./ToggleSwitch";
import LeverageButtons from "./LeverageButtons";
import LeverageSlider from "./LeverageSlider";
import TriggerButtons from "./TriggerButtons";
import CustomSelect from "./CustomSelect";

const options = ["ETH", "USDC"];

type SizeInputProps = {
  isLong: boolean;
  activeType: string;
};

const SizeInput: React.FC<SizeInputProps> = ({ isLong, activeType }) => {
  const [collateral, setCollateral] = useState("");
  const [leverage, setLeverage] = useState(2);
  const [collateralType, setCollateralType] = useState("ETH");
  const [useSlider, setUseSlider] = useState(false);
  const [stopLossEnabled, setStopLossEnabled] = useState(false);
  const [takeProfitEnabled, setTakeProfitEnabled] = useState(false);
  const [collateralImage, setCollateralImage] = useState(
    "/img/trade/ETH-Logo.png"
  );

  const handleCollateralChange = (event: any) => {
    setCollateral(event.target.value);
  };

  const handleLeverageChange = (value: any) => {
    setLeverage(value);
  };

  const handleCollateralTypeChange = (selectedType: any) => {
    setCollateralType(selectedType);
    setCollateralImage(
      selectedType === "ETH"
        ? "/img/trade/ETH-Logo.png"
        : "/img/trade/USDC-Logo.png"
    );
  };

  return (
    <div className="flex flex-col gap-4 rounded-lg">
      <div className="flex justify-between items-center py-5 px-3 bg-input-grad border-cardborder border-2 rounded-lg">
        <div>
          <label className="block text-printer-gray text-xs mb-2">
            Pay: ${parseFloat(collateral) * leverage}
          </label>
          <input
            type="number"
            value={collateral}
            onChange={handleCollateralChange}
            className="bg-transparent outline-none focus:outline-none focus:ring-0 ring-0 w-full min-w-32 font-bold text-lg  text-printer-gray"
            placeholder="0.0"
          />
        </div>
        <div className="flex flex-col">
          <div className="flex flex-row text-printer-gray text-xs gap-2 mb-2">
            <span>Balance:</span>
            <span className="font-bold">1000</span>
          </div>
          <div className="flex flex-row w-full justify-end items-center gap-2">
            <Image
              src={collateralImage}
              alt={collateralType}
              width={24}
              height={24}
            />
            <CustomSelect
              options={options}
              selectedOption={collateralType}
              onOptionSelect={handleCollateralTypeChange}
            />
          </div>
        </div>
      </div>
      <div className="flex justify-between items-center py-5 px-3 bg-input-grad border-cardborder border-2 rounded-lg">
        <div>
          <label className="block text-printer-gray text-xs mb-2">
            {isLong ? "Long" : "Short"}: ${parseFloat(collateral) * leverage}
          </label>
          <div className="text-white text-lg">
            {parseFloat(collateral) * leverage}
          </div>
        </div>
        <div className="flex flex-col">
          <div className="flex flex-row text-printer-gray text-xs gap-2 mb-2">
            <span>Leverage:</span>
            <span className="font-bold">{leverage}x</span>
          </div>
          <div className="flex flex-row w-full justify-end items-center gap-2">
            <Image
              src="/img/trade/BTC-Logo.png"
              alt="BTC"
              width={24}
              height={24}
            />
            <span className="font-bold text-lg text-printer-gray">BTC</span>
          </div>
        </div>
      </div>
      <div className="flex justify-between items-center">
        <label className="block text-gray-400 text-[15px] mb-2">
          <span className="text-gray-text">Leverage</span> Up to{" "}
          <span className="font-bold">10x</span>
        </label>
        <ToggleSwitch
          value={useSlider}
          setValue={setUseSlider}
          label="Slider"
        />
      </div>
      <div className="h-10 mb-2">
        {useSlider ? (
          <LeverageSlider
            min={1.1}
            max={10}
            step={0.1}
            initialValue={1.1}
            onChange={handleLeverageChange}
            isLongPosition={isLong}
          />
        ) : (
          <LeverageButtons
            onChange={(event: any) =>
              handleLeverageChange(Number(event.target.value))
            }
            maxLeverage={10}
            isLongPosition={isLong}
          />
        )}
      </div>
      <div className="flex flex-col gap-4">
        <div className="flex justify-between items-center">
          <span className="text-gray-400 text-[15px]">Stop Loss</span>
          <ToggleSwitch
            value={stopLossEnabled}
            setValue={setStopLossEnabled}
            label=""
          />
        </div>
        {stopLossEnabled && (
          <TriggerButtons
            onChange={(event: any) => {}}
            onPriceChange={(event: any) => {}}
            isLongPosition={isLong}
          />
        )}
        <div className="flex justify-between items-center">
          <span className="text-gray-400 text-[15px]">Take Profit</span>
          <ToggleSwitch
            value={takeProfitEnabled}
            setValue={setTakeProfitEnabled}
            label=""
          />
        </div>
        {takeProfitEnabled && (
          <TriggerButtons
            onChange={(event: any) => {}}
            onPriceChange={(event: any) => {}}
            isLongPosition={isLong}
          />
        )}
      </div>
    </div>
  );
};

export default SizeInput;

And here are the sub-components, within the SizeInput component.

ToggleSwitch:

import React from "react";

type ToggleSwitchProps = {
  value: boolean;
  setValue: (value: boolean) => void;
  label: string;
};

const ToggleSwitch: React.FC<ToggleSwitchProps> = ({
  value,
  setValue,
  label,
}) => {
  return (
    <label className="inline-flex gap-2 items-center me-5 cursor-pointer">
      <span className="text-[15px] font-medium text-gray-text">{label}</span>
      <input
        type="checkbox"
        value=""
        className="sr-only peer"
        checked={value}
        onChange={() => setValue(!value)}
      />
      <div className="relative w-10 h-5 rounded-full peer border-cardborder border-2 bg-gradient-to-b from-input-top to-input-bottom peer-checked:after:translate-x-5 peer-checked:after:border-white after:content-[''] after:absolute after:top-[-2px] after:left-[-1px] after:bg-[#E9EDF0] after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:from-printer-orange peer-checked:to-printer-light-orange"></div>
    </label>
  );
};

export default ToggleSwitch;

TriggerButtons

import React from "react";

type TriggerButtonsProps = {
  onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
  onPriceChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
  isLongPosition: boolean;
};

const TriggerButtons: React.FC<TriggerButtonsProps> = ({
  onChange,
  onPriceChange,
  isLongPosition,
}) => {
  return (
    <div className="flex flex-col gap-2">
      <div className="flex gap-2">
        {["25%", "50%", "75%", "100%", "Custom"].map((option) => (
          <button key={option} className="p-2 bg-input-grad rounded">
            {option}
          </button>
        ))}
      </div>
      <input
        type="number"
        onChange={onPriceChange}
        placeholder="Trigger Price"
        className="p-2 bg-input-grad rounded"
      />
    </div>
  );
};

export default TriggerButtons;

LeverageSlider

import React from "react";

type LeverageSliderProps = {
  min: number;
  max: number;
  step: number;
  initialValue: number;
  onChange: (value: number) => void;
  isLongPosition: boolean;
};

const LeverageSlider: React.FC<LeverageSliderProps> = ({
  min,
  max,
  step,
  initialValue,
  onChange,
  isLongPosition,
}) => {
  return (
    <input
      type="range"
      min={min}
      max={max}
      step={step}
      defaultValue={initialValue}
      onChange={(event) => onChange(Number(event.target.value))}
      className="w-full"
    />
  );
};

export default LeverageSlider;

LeverageButtons

import React from "react";

type LeverageButtonsProps = {
  onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
  maxLeverage: number;
  isLongPosition: boolean;
};

const LeverageButtons: React.FC<LeverageButtonsProps> = ({
  onChange,
  maxLeverage,
  isLongPosition,
}) => {
  const handleButtonClick = (value: any) => {};

  return (
    <div className="flex gap-2">
      {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((val) => (
        <button
          key={val}
          onClick={() => handleButtonClick(val)}
          className={`p-2 rounded ${
            val <= maxLeverage ? "bg-input-grad" : "bg-gray-300"
          }`}
          disabled={val > maxLeverage}
        >
          {val}x
        </button>
      ))}
    </div>
  );
};

export default LeverageButtons;

CustomSelect

import React from "react";

type CustomSelectProps = {
  options: string[];
  selectedOption: string;
  onOptionSelect: (option: string) => void;
};

const CustomSelect: React.FC<CustomSelectProps> = ({
  options,
  selectedOption,
  onOptionSelect,
}) => {
  return (
    <select
      value={selectedOption}
      onChange={(e) => onOptionSelect(e.target.value)}
      className="p-2 bg-input-grad rounded"
    >
      {options.map((option) => (
        <option key={option} value={option}>
          {option}
        </option>
      ))}
    </select>
  );
};

export default CustomSelect;

I’ve attached a video displaying the issue: https://www.loom.com/share/4dbd8734b8044b3b8f4a2edace95278d?sid=34c1e025-c676-4b96-9835-ba019dc271cf

I have tried:

  • Setting heights to auto
  • Using flex grow
  • Using overflow: hidden on the body (the screen gets pushed downwards as the whitespace appears and fixes the screen half filled by whitespace, without the ability to scroll)

And a few other methods.

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật