Skip to content

galangel/react-scroll-magic

React Scroll Magic

πŸͺ„ React Scroll Magic

Create magical scroll experiences with nested sticky headers, collapsible sections, and smooth animations.

npm version npm downloads license TypeScript React 18+

πŸ“– Live Demo & Documentation


✨ Key Features

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

πŸš€ Quick Start

Installation

npm install @galangel/react-scroll-magic

or

yarn add @galangel/react-scroll-magic

Basic Usage

import { 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>
  );
}

πŸ“š API Reference

Scroll Component Props

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

Item Structure

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)
}

Loading Type Definition

interface Loading {
  onBottomReached?: () => Promise<void>; // Callback when user scrolls to bottom
  render?: (isLoading: boolean) => JSX.Element; // Custom loading indicator renderer
}

♾️ Infinite Scrolling Example

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>
  );
};

🎨 Styling & CSS Classes

The component uses semantic CSS classes that you can target for custom styling. Here's a complete reference:

CSS Class 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

Styling Examples

/* 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 Example

/* 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;
}

Important Notes

⚠️ 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>

πŸ’‘ Tips & Best Practices

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

πŸ€– Real World Example: AI Chat

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

πŸ“„ License

This project is licensed under the Apache License 2.0. See the LICENSE file for details.


🀝 Contributing

Contributions are welcome! Please read the CONTRIBUTING guidelines before submitting a pull request.


πŸ’¬ Contact

For any questions or feedback, please open an issue on GitHub.


Made with πŸͺ„ by @galangel

Buy Me A Coffee

Sponsor this project

  •  

Packages

No packages published

Contributors 2

  •  
  •  

Languages