Position fixed element inside scrollbar. #362
-
Hello, Setting position: fixed, top: 0, left: 0, right: 0 didn't work. |
Beta Was this translation helpful? Give feedback.
Replies: 7 comments 3 replies
-
Since So it's recommended to put your fixed element outside the scrollbar contents, or you may need to register a scroll listener and apply offsets to the fixed element. FYI: const scrollbar = Scrollbar.init(elem, {
// execute listeners synchronously to make sure they can be rendered at same tick
// this is only needed in v7.x
syncCallbacks: true,
});
scrollbar.addListener(({ offset }) => {
fixed.style.top = offset.y + 'px';
fixed.style.left = offset.x + 'px';
}); A working demo is here: http://jsbin.com/tuqafof/edit 06/04/2022 Edit |
Beta Was this translation helpful? Give feedback.
-
@idiotWu great, thank you very much, works perfectly! |
Beta Was this translation helpful? Give feedback.
-
@idiotWu thank you so much! you save me a lot of hours!!! :D |
Beta Was this translation helpful? Give feedback.
-
Perfect hack ! Thanks a lot!! |
Beta Was this translation helpful? Give feedback.
-
If your SS is placed in #demo, you can add the following style to the #demo #demo{
overflow: auto; height: 100vh;
}
.your_fixed{
position:fixed;
} |
Beta Was this translation helpful? Give feedback.
-
thnx idom |
Beta Was this translation helpful? Give feedback.
-
Sticky PositioningThis was one of the most time consuming and frustrating things I have had to deal with in a long time. I spent many hours on this issue. For others' benefit, here is a React (built for Next.js) component that will wrap your scrollable content and handle sticky headings exactly the same way CSS3 does (and plays nicely with Tailwind): // SmoothScrollContainer.tsx
'use client';
import { ElementType, forwardRef, useEffect, useRef, ReactNode, ComponentPropsWithoutRef } from 'react';
import Scrollbar from 'smooth-scrollbar';
// Use this if you want to automatically disable `smooth-scrollbar` for mobile devices - see comment
const disableForMobileClients = true;
// Define the props for the SmoothScrollContainer component
interface SmoothScrollContainerProps<T extends ElementType> extends ComponentPropsWithoutRef<T> {
as?: T;
maxHeight?: string;
children: ReactNode;
}
/**
* A container component that applies smooth scrolling to its content.
*
* This component uses the Smooth Scrollbar library to create a smooth scrolling experience
* for its children. It also supports sticky headers within the container.
*
* @param {ElementType} as The element type to render as (default: 'div')
* @param {string} maxHeight The maximum height of the container
* @param {ReactNode} children The content to render within the container
* @param {CSSProperties} style Additional styles to apply to the container
* @returns {JSX.Element} A container component with smooth scrolling
*
* @example <SmoothScrollContainer as="div" maxHeight="27rem" className="max-h-[27rem] overflow-y-auto">Some content here that is very long ...</SmoothScrollContainer>
*/
const SmoothScrollContainer = forwardRef(
<T extends ElementType = 'div'>(
{ as: Component = 'div', maxHeight, children, style, ...props }: SmoothScrollContainerProps<T>,
ref
) => {
// References to the container and scroll content elements
const containerRef = useRef<HTMLDivElement>(null);
const absoluteRef = useRef<HTMLDivElement>(null);
const combinedRef = ref || containerRef;
// Store references to sticky headers and their calculated positions
const headers = useRef<HTMLElement[]>([]);
const headerPositions = useRef<number[]>([]); // Store the top positions of the headers
// Initialize the smooth-scrollbar instance when the component mounts
useEffect(() => {
// Check if the containerRef is available
if (containerRef.current &&
// Check if the device is not a mobile device or smooth scrolling is enabled
((disableForMobileClients && !isMobile()) || !disableForMobileClients)
) {
// Initialize smooth-scrollbar with a damping factor for smoothness
const scrollbar = Scrollbar.init(containerRef.current, {
damping: 0.07 // Adjust the damping factor for scroll smoothness
});
// Get all sticky elements within the container
const stickyElements = containerRef.current.querySelectorAll('.sticky');
// Skip sticky logic if no sticky elements exist
if (stickyElements.length > 0) {
// Collect all sticky headers
headers.current = Array.from(stickyElements) as HTMLElement[];
// Iterate over each sticky element to create placeholders and apply transformations
stickyElements.forEach((element, index) => {
const sticky = element as HTMLElement;
// Check if the next sibling is already a placeholder to avoid duplication
const nextSibling = sticky.nextElementSibling;
if (nextSibling && nextSibling.classList.contains('sticky-placeholder')) {
return; // Placeholder already exists, skip
}
// Create a placeholder for the sticky element to avoid layout shifts
const placeholder = document.createElement('div');
placeholder.style.height = `${sticky.offsetHeight}px`; // Match the height of the sticky element
placeholder.style.visibility = 'hidden'; // Make the placeholder invisible
placeholder.classList.add('sticky-placeholder'); // Add a class for identification
// Insert the placeholder right after the sticky element
sticky.parentElement?.insertBefore(placeholder, sticky.nextSibling);
// Wait for the next frame to ensure the DOM has updated
requestAnimationFrame(() => {
// Get the position of the placeholder relative to the viewport
const placeholderPosition = placeholder.getBoundingClientRect();
// Get the offsetTop of the placeholder relative to the container
const placeholderOffsetTop = placeholder.offsetTop;
// Get the current scroll position of the container
const containerScrollTop = absoluteRef.current!.scrollTop;
// Calculate the top position of the placeholder relative to the container's content area
const placeholderTopRelativeToContainer = (placeholderOffsetTop - placeholderPosition.height) - containerScrollTop;
// Apply the translate3d transformation to the sticky element based on the adjusted position
sticky.style.transform = `translate3d(0, ${placeholderTopRelativeToContainer}px, 0)`;
sticky.style.zIndex = '1000'; // Ensure sticky elements stay above other content
sticky.style.position = 'absolute'; // Set absolute positioning within the container
sticky.style.width = '100%'; // Maintain the full width of the element
// Store the position of the placeholder in headerPositions
if (!headerPositions.current) {
headerPositions.current = [];
}
headerPositions.current[index] = placeholderTopRelativeToContainer;
});
});
// Scroll handling logic to make headers sticky during scrolling
const handleSticky = ({ offset }) => {
const scrollPosition = offset.y;
// Iterate over each sticky header to apply transformations
headers.current.forEach((header, index) => {
// Get the next header position (or Infinity if it doesn't exist)
const nextHeaderPosition = headerPositions.current[index + 1] || Infinity;
const currentHeaderPosition = headerPositions.current[index];
// Make the header sticky if the scroll position is within its range
if (scrollPosition >= currentHeaderPosition && scrollPosition < nextHeaderPosition) {
header.style.transform = `translate3d(0, ${offset.y}px, 0)`;
header.style.zIndex = '1000';
header.style.position = 'absolute';
header.style.top = '0px';
header.style.width = '100%';
}
});
};
// Attach the scroll listener to the scrollbar instance
scrollbar.addListener(handleSticky);
// Perform an initial check to apply the correct transformations based on the initial scroll position
handleSticky({ offset: { x: 0, y: 0 } });
}
// Cleanup function to destroy the scrollbar instance when the component unmounts
return () => {
scrollbar.destroy();
};
}
}, []);
// Ensure that Component is either an intrinsic element (string) or a valid React component
const Tag = Component as ElementType;
// Determine the styles to apply
let applyStyles = {};
if ((disableForMobileClients && !isMobile()) || !disableForMobileClients) {
// Apply styles for the container (the device is either not a mobile device or smooth scrolling is enabled)
applyStyles = {
maxHeight: maxHeight || 'none', // Apply maxHeight if provided
overflow: 'hidden', // Smooth Scrollbar takes over scrolling
position: 'relative', // Ensure relative positioning for internal elements
...style, // Allow custom styles to be merged
};
}
return (
<Tag
{...props}
ref={combinedRef}
style={applyStyles}
>
{/* Scroll content container with ref for absolute positioning */}
<div className="scroll-content relative" ref={absoluteRef}>
{children}
</div>
</Tag>
);
}
);
/**
* Check if the current device is a mobile device.
*
* @returns {boolean} A boolean value indicating if the current device is a mobile device
*/
const isMobile = (): boolean => {
// Check user agent for mobile devices
const userAgent = typeof navigator === 'undefined' ? '' : navigator.userAgent;
const mobileRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
// Check for touch capabilities
const isTouchDevice =
typeof window !== 'undefined' && ('ontouchstart' in window || navigator.maxTouchPoints > 0);
// Additional check based on screen width
const isSmallScreen = typeof window !== 'undefined' && window.innerWidth <= 768;
// Return true if any of the checks are true
return mobileRegex.test(userAgent) || isTouchDevice || isSmallScreen;
};
// Set the display name for the component for easier debugging
SmoothScrollContainer.displayName = 'SmoothScrollContainer';
// Export the SmoothScrollContainer component
export default SmoothScrollContainer; You can use it just like you do any other imported component: import SmoothScrollContainer from "./path/to/SmoothScrollContainer";
<SmoothScrollContainer as="div" maxHeight="27rem" className="max-h-[27rem] overflow-y-auto scroll-container">
...
</SmoothScrollContainer> or you can go naked import SmoothScrollContainer from "./path/to/SmoothScrollContainer";
<SmoothScrollContainer>
...
</SmoothScrollContainer> By default, this will disable Please, go out, enjoy the fruits of my labour. Happy coding! |
Beta Was this translation helpful? Give feedback.
Since
transform
creates a new local coordinate system(W3C Spec),position: fixed
is fixed to the origin of scrollbar content container, i.e. theleft: 0, top: 0
point.So it's recommended to put your fixed element outside the scrollbar contents, or you may need to register a scroll listener and apply offsets to the fixed element.
FYI:
A working demo is here: http://jsbin.com/tuqafo…