Create magical scroll experiences with nested sticky headers, collapsible sections, and smooth animations.
π Live Demo & Documentation
| Feature | Description |
|---|---|
| π Sticky Headers | Headers stick to top as you scroll, with support for nested sticky behavior |
| π― Nested Structure | Create deeply nested hierarchies with items inside items |
| π¦ Collapse/Expand | Each section with nested content can be collapsed or expanded |
| βΎοΈ Infinite Scrolling | Built-in support for loading more items when reaching the bottom |
| π¨ Fully Customizable | Complete control over rendering via render props |
| π€ TypeScript Ready | Fully typed with comprehensive interfaces |
npm install @galangel/react-scroll-magicor
yarn add @galangel/react-scroll-magicimport { Scroll } from '@galangel/react-scroll-magic';
const items = [
{
id: 'section-1',
render: ({ collapse }) => (
<div style={{ padding: '10px', backgroundColor: '#f0f0f0' }}>
Header 1
{collapse && (
<button onClick={collapse.isOpen ? collapse.close : collapse.open}>{collapse.isOpen ? 'βΌ' : 'βΆ'}</button>
)}
</div>
),
nestedItems: [
{ render: () => <div style={{ padding: '10px' }}>Item 1.1</div> },
{ render: () => <div style={{ padding: '10px' }}>Item 1.2</div> },
],
},
{
id: 'section-2',
render: () => <div style={{ padding: '10px' }}>Simple Item</div>,
},
];
function App() {
return (
<div style={{ height: '400px', width: '100%' }}>
<Scroll items={items} headerBehavior="push" scrollBehavior="smooth" />
</div>
);
}| Prop | Type | Default | Description |
|---|---|---|---|
items |
Items |
Required | Array of items to render. Each item has a render function and optional nestedItems |
stickTo |
'top' | 'bottom' | 'all' |
'all' |
Where headers should stick when scrolling |
scrollBehavior |
'auto' | 'instant' | 'smooth' |
'smooth' |
CSS scroll-behavior when clicking headers |
headerBehavior |
'stick' | 'push' | 'none' |
'none' |
How headers behave when scrolling |
loading |
Loading |
Optional | Configuration for infinite scrolling |
interface Item {
id?: string; // Optional unique identifier
render: (props: {
// Render function for the item
collapse?: {
isOpen: boolean; // Current collapse state
open: () => void; // Function to expand
close: () => void; // Function to collapse
};
}) => JSX.Element;
nestedItems?: Item[]; // Optional nested items (makes this a header)
}interface Loading {
onBottomReached?: () => Promise<void>; // Callback when user scrolls to bottom
render?: (isLoading: boolean) => JSX.Element; // Custom loading indicator renderer
}import React, { useState } from 'react';
import { Scroll } from '@galangel/react-scroll-magic';
const InfiniteScrollExample = () => {
const [items, setItems] = useState([
{ render: () => <div>Initial Item 1</div> },
{ render: () => <div>Initial Item 2</div> },
]);
const loadMoreItems = async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
const newItems = Array.from({ length: 10 }, (_, i) => ({
render: () => <div>New Item {items.length + i + 1}</div>,
}));
setItems((prev) => [...prev, ...newItems]);
};
const loading = {
onBottomReached: loadMoreItems,
render: (isLoading) => (
<div style={{ textAlign: 'center', padding: '20px' }}>{isLoading ? 'Loading...' : 'Load more'}</div>
),
};
return (
<div style={{ height: '400px', width: '100%' }}>
<Scroll items={items} loading={loading} headerBehavior="none" />
</div>
);
};The component uses semantic CSS classes that you can target for custom styling. Here's a complete reference:
| Class Name | Element | Description |
|---|---|---|
.scroll-list |
<ul> |
Main scroll container element |
.scroll-item |
<li> |
Regular list item (items without nestedItems) |
.scroll-header |
<li> |
Header item (items with nestedItems) |
.scroll-header.stick |
<li> |
Header with headerBehavior="stick" |
.scroll-header.push |
<li> |
Header with headerBehavior="push" |
.scroll-header.none |
<li> |
Header with headerBehavior="none" |
.scroll-loading |
<li> |
Loading indicator container |
.scroll-loading.loading |
<li> |
Loading indicator when actively loading |
/* Main scroll container */
.scroll-list {
list-style: none;
margin: 0;
padding: 0;
height: 100%;
overflow-y: auto;
}
/* All items (headers and regular items) */
.scroll-item,
.scroll-header {
width: 100%;
box-sizing: border-box;
}
/* Regular items */
.scroll-item {
padding: 12px 16px;
background-color: #fff;
border-bottom: 1px solid #eee;
}
/* Header items - base styles */
.scroll-header {
padding: 16px 20px;
background-color: #f5f5f5;
font-weight: 600;
cursor: pointer;
border-bottom: 1px solid #ddd;
}
/* Sticky header behavior */
.scroll-header.stick {
position: sticky;
/* top/bottom values are set dynamically by the component */
}
/* Push header behavior */
.scroll-header.push {
position: sticky;
/* top value is set dynamically by the component */
}
/* Header hover effect */
.scroll-header:hover {
background-color: #e8e8e8;
}
/* Loading indicator */
.scroll-loading {
display: none;
padding: 20px;
text-align: center;
}
.scroll-loading.loading {
display: block;
}
/* Loading spinner animation */
.scroll-loading.loading::after {
content: '';
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid #ccc;
border-top-color: #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}/* Dark theme styling */
.scroll-list {
background-color: #1a202c;
}
.scroll-item {
background-color: #2d3748;
color: #e2e8f0;
border-bottom: 1px solid #4a5568;
}
.scroll-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.scroll-header:hover {
filter: brightness(1.1);
}
.scroll-loading.loading::after {
border-color: #4a5568;
border-top-color: #667eea;
}
β οΈ Container Height Required: The scroll container must have a defined height for scrolling to work properly.
<div style={{ height: '400px' }}>
{' '}
{/* or height: '100vh' */}
<Scroll items={items} />
</div>| Tip | Description |
|---|---|
| π― Use Unique IDs | Assign unique id properties to items for better performance and scroll-to functionality |
| π Stop Propagation | When adding click handlers inside items (like collapse buttons), use e.stopPropagation() to prevent scroll-to behavior |
| π Set Container Height | The Scroll component needs a container with a defined height (height: 100vh or fixed pixels) |
| π¨ headerBehavior: "push" | The "push" mode creates a natural feel where headers push each other out of view |
Check out the AI Chat demo showcasing a complex real-world use case with:
- π¬ Question β Response Flow: Messages with nested reasoning steps
- π§ Collapsible Reasoning: Auto-collapse when complete
- π Deep Nesting: Four levels of nesting working seamlessly
This project is licensed under the Apache License 2.0. See the LICENSE file for details.
Contributions are welcome! Please read the CONTRIBUTING guidelines before submitting a pull request.
For any questions or feedback, please open an issue on GitHub.
Made with πͺ by @galangel
