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.