diff --git a/ReadMe.md b/ReadMe.md
index e00e7a3..2d96d69 100644
--- a/ReadMe.md
+++ b/ReadMe.md
@@ -1,6 +1,6 @@
 # MobX RESTful table
 
-A super **Table component** for [CRUD operation][1], which is based on [MobX RESTful][2] & [React][3].
+A **Pagination Table** & **Scroll List** component suite for [CRUD operation][1], which is based on [MobX RESTful][2] & [React][3].
 
 [![CI & CD](https://github.com/idea2app/MobX-RESTful-table/actions/workflows/main.yml/badge.svg)][4]
 
@@ -46,6 +46,8 @@ npm i react@17 \
 
 ## Initialization
 
+### Pagination Table
+
 - [Source Code][10]
 - [Preview Link][11]
 
@@ -131,6 +133,81 @@ export default class PaginationPage extends PureComponent {
 }
 ```
 
+### Scroll List
+
+[Preview Link][12]
+
+#### `components/Git/index.tsx`
+
+[Source Code][13]
+
+```tsx
+import { observer } from 'mobx-react';
+import { Row, Col } from 'react-bootstrap';
+import { ScrollList, ScrollListProps } from 'mobx-restful-table';
+
+import { GitCard } from './Card';
+import { GitRepository, RepositoryModel } from '../../models/Repository';
+import { i18n } from '../../models/Translation';
+
+export interface GitListProps extends ScrollListProps<GitRepository> {
+  store: RepositoryModel;
+}
+
+@observer
+export class GitList extends ScrollList<GitListProps> {
+  store = this.props.store;
+  translater = i18n;
+
+  constructor(props: GitListProps) {
+    super(props);
+
+    this.boot();
+  }
+
+  renderList() {
+    const { allItems } = this.store;
+
+    return (
+      <Row as="ul" className="list-unstyled g-4" xs={1} sm={2}>
+        {allItems.map(item => (
+          <Col as="li" key={item.id}>
+            <GitCard className="h-100 shadow-sm" {...item} />
+          </Col>
+        ))}
+      </Row>
+    );
+  }
+}
+```
+
+#### `pages/scroll-list.tsx`
+
+[Source Code][14]
+
+```tsx
+import { observer } from 'mobx-react';
+import { FC } from 'react';
+import { Container } from 'react-bootstrap';
+import { Loading } from 'idea-react';
+
+import { GitList } from '../components/Git';
+import repositoryStore from '../models/Repository';
+import { i18n } from '../models/Translation';
+
+const ScrollListPage: FC = observer(() => (
+  <Container>
+    <h1 className="my-4">{i18n.t('scroll_list')}</h1>
+
+    {repositoryStore.downloading > 0 && <Loading />}
+
+    <GitList store={repositoryStore} />
+  </Container>
+));
+
+export default ScrollListPage;
+```
+
 [1]: https://en.wikipedia.org/wiki/Create,_read,_update_and_delete
 [2]: https://github.com/idea2app/MobX-RESTful
 [3]: https://reactjs.org/
@@ -142,3 +219,6 @@ export default class PaginationPage extends PureComponent {
 [9]: https://github.com/idea2app/Next-Bootstrap-TS/blob/main/models/Repository.ts
 [10]: https://github.com/idea2app/Next-Bootstrap-TS/blob/main/pages/pagination.tsx
 [11]: https://next-bootstrap-ts.vercel.app/pagination/
+[12]: https://next-bootstrap-ts.vercel.app/scroll-list/
+[13]: https://github.com/idea2app/Next-Bootstrap-TS/blob/scroll-list-example/components/Git
+[14]: https://github.com/idea2app/Next-Bootstrap-TS/blob/scroll-list-example/pages/scroll-list.tsx
diff --git a/package.json b/package.json
index 6119012..f68eab1 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "mobx-restful-table",
-  "version": "0.4.1",
+  "version": "0.5.0",
   "license": "LGPL-3.0",
   "author": "shiy2008@gmail.com",
   "description": "A super Table component for CRUD operation, which is based on MobX RESTful & React.",
@@ -29,9 +29,9 @@
     "@swc/helpers": "^0.4.14",
     "classnames": "^2.3.2",
     "lodash": "^4.17.21",
-    "mobx-i18n": "^0.3.6",
+    "mobx-i18n": "^0.3.11",
     "mobx-react": "^6.3.1",
-    "mobx-restful": "^0.6.0-rc.19",
+    "mobx-restful": "^0.6.0-rc.21",
     "react-bootstrap": "^2.7.0",
     "regenerator-runtime": "^0.13.11",
     "web-utility": "^3.9.9"
@@ -51,7 +51,7 @@
     "parcel": "~2.6.2",
     "prettier": "^2.8.1",
     "react": "^17.0.2",
-    "typedoc": "^0.23.21",
+    "typedoc": "^0.23.22",
     "typedoc-plugin-mdn-links": "^2.0.0",
     "typescript": "~4.7.4"
   },
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ff34808..b8e1be0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -11,15 +11,15 @@ specifiers:
   lint-staged: ^13.1.0
   lodash: ^4.17.21
   mobx: ^5.15.7
-  mobx-i18n: ^0.3.6
+  mobx-i18n: ^0.3.11
   mobx-react: ^6.3.1
-  mobx-restful: ^0.6.0-rc.19
+  mobx-restful: ^0.6.0-rc.21
   parcel: ~2.6.2
   prettier: ^2.8.1
   react: ^17.0.2
   react-bootstrap: ^2.7.0
   regenerator-runtime: ^0.13.11
-  typedoc: ^0.23.21
+  typedoc: ^0.23.22
   typedoc-plugin-mdn-links: ^2.0.0
   typescript: ~4.7.4
   web-utility: ^3.9.9
@@ -28,9 +28,9 @@ dependencies:
   '@swc/helpers': 0.4.14
   classnames: 2.3.2
   lodash: 4.17.21
-  mobx-i18n: 0.3.6_mobx@5.15.7
+  mobx-i18n: 0.3.11_mobx@5.15.7
   mobx-react: 6.3.1_mobx@5.15.7+react@17.0.2
-  mobx-restful: 0.6.0-rc.19_dss5dgebhlll4wuvbmbwonwy4e
+  mobx-restful: 0.6.0-rc.21_dss5dgebhlll4wuvbmbwonwy4e
   react-bootstrap: 2.7.0_q5o373oqrklnndq2vhekyuzhxi
   regenerator-runtime: 0.13.11
   web-utility: 3.9.9_typescript@4.7.4
@@ -46,8 +46,8 @@ devDependencies:
   parcel: 2.6.2
   prettier: 2.8.1
   react: 17.0.2
-  typedoc: 0.23.21_typescript@4.7.4
-  typedoc-plugin-mdn-links: 2.0.0_typedoc@0.23.21
+  typedoc: 0.23.22_typescript@4.7.4
+  typedoc-plugin-mdn-links: 2.0.0_typedoc@0.23.22
   typescript: 4.7.4
 
 packages:
@@ -1014,6 +1014,10 @@ packages:
   /@types/scheduler/0.16.2:
     resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==}
 
+  /@types/validator/13.7.10:
+    resolution: {integrity: sha512-t1yxFAR2n0+VO6hd/FJ9F2uezAZVWHLmpmlJzm1eX03+H7+HsuTAp7L8QJs+2pQCfWkP1+EXsGK9Z9v7o/qPVQ==}
+    dev: false
+
   /@types/warning/3.0.0:
     resolution: {integrity: sha512-t/Tvs5qR47OLOr+4E9ckN8AmP2Tf16gWq+/qA4iUGS/OOyHVO8wv2vjJuX8SNOUTJyWb+2t7wJm6cXILFnOROA==}
     dev: false
@@ -1150,9 +1154,10 @@ packages:
     engines: {node: '>=6.0'}
     dev: true
 
-  /class-validator/0.13.2:
-    resolution: {integrity: sha512-yBUcQy07FPlGzUjoLuUfIOXzgynnQPPruyK1Ge2B74k9ROwnle1E+NxLWnUv5OLU8hA/qL5leAE9XnXq3byaBw==}
+  /class-validator/0.14.0:
+    resolution: {integrity: sha512-ct3ltplN8I9fOwUd8GrP8UQixwff129BkEtuWDKL5W45cQuLd19xqmTLu5ge78YDm/fdje6FMt0hGOhl0lii3A==}
     dependencies:
+      '@types/validator': 13.7.10
       libphonenumber-js: 1.10.14
       validator: 13.7.0
     dev: false
@@ -1810,8 +1815,8 @@ packages:
       brace-expansion: 2.0.1
     dev: true
 
-  /mobx-i18n/0.3.6_mobx@5.15.7:
-    resolution: {integrity: sha512-hfa+TUaIpgGAGIJ0tq6WOHYNTrTHPt6bUHbAwNxbyiP+puCnYAA8EnKyBLXALUi+cvTB4jtv6MgJueoPjI96rQ==}
+  /mobx-i18n/0.3.11_mobx@5.15.7:
+    resolution: {integrity: sha512-TMh5db8P5bP3i3atqGeF3HQD2L/AEd6YcNsgJlEhDjFyMmmq9knlFWuIJ2CDo3CDh4MMbHXP+jNVZBBJfonGoA==}
     peerDependencies:
       mobx: '>=4 <6'
     dependencies:
@@ -1851,13 +1856,13 @@ packages:
       - react-native
     dev: false
 
-  /mobx-restful/0.6.0-rc.19_dss5dgebhlll4wuvbmbwonwy4e:
-    resolution: {integrity: sha512-88o3fJmSZRoJNpIZWMgum/DgKFxDxYRZhrEr6ovhZOG+346Ahl54x41CxDHQcu7NRFwpxyMwO0j1Ixjw9DSK9A==}
+  /mobx-restful/0.6.0-rc.21_dss5dgebhlll4wuvbmbwonwy4e:
+    resolution: {integrity: sha512-lB4Osl2bDOGYC8zdxs0xvKK5ZyoowlOfRERRhOoupoQP10Sr5mtvmXpFwYjOtl9woD+kf9lfFTotx4bEP80OrQ==}
     peerDependencies:
       mobx: '>=4 <6'
     dependencies:
       '@swc/helpers': 0.4.14
-      class-validator: 0.13.2
+      class-validator: 0.14.0
       koajax: 0.8.3_typescript@4.7.4
       mobx: 5.15.7
       reflect-metadata: 0.1.13
@@ -2393,16 +2398,16 @@ packages:
     engines: {node: '>=10'}
     dev: true
 
-  /typedoc-plugin-mdn-links/2.0.0_typedoc@0.23.21:
+  /typedoc-plugin-mdn-links/2.0.0_typedoc@0.23.22:
     resolution: {integrity: sha512-IGLuelXPOenGdmklr5DHgPPf/MfZj7aEYCxCMtPN8C1D0lA7w0YLahd0jhDDcOMU7zL1EPcM5pPnhZHltDhqGQ==}
     peerDependencies:
       typedoc: 0.22.x || 0.23.x
     dependencies:
-      typedoc: 0.23.21_typescript@4.7.4
+      typedoc: 0.23.22_typescript@4.7.4
     dev: true
 
-  /typedoc/0.23.21_typescript@4.7.4:
-    resolution: {integrity: sha512-VNE9Jv7BgclvyH9moi2mluneSviD43dCE9pY8RWkO88/DrEgJZk9KpUk7WO468c9WWs/+aG6dOnoH7ccjnErhg==}
+  /typedoc/0.23.22_typescript@4.7.4:
+    resolution: {integrity: sha512-5sJkjK60xp8A7YpcYniu3+Wf0QcgojEnhzHuCN+CkdpQkKRhOspon/9+sGTkGI8kjVkZs3KHrhltpQyVhRMVfw==}
     engines: {node: '>= 14.14'}
     hasBin: true
     peerDependencies:
diff --git a/source/RestTable.tsx b/source/RestTable.tsx
index f2bf878..6fdd92f 100644
--- a/source/RestTable.tsx
+++ b/source/RestTable.tsx
@@ -101,6 +101,7 @@ export class RestTable<T extends DataObject> extends PureComponent<
                   checkedKeys.includes(ID),
                 )
               }
+              // https://github.com/facebook/react/issues/1798
               ref={(input: HTMLInputElement | null) =>
                 input &&
                 (input.indeterminate =
diff --git a/source/ScrollBoundary.tsx b/source/ScrollBoundary.tsx
new file mode 100644
index 0000000..3af09a7
--- /dev/null
+++ b/source/ScrollBoundary.tsx
@@ -0,0 +1,60 @@
+import classNames from 'classnames';
+import { FC, ReactNode } from 'react';
+
+export type EdgePosition = 'top' | 'bottom' | 'left' | 'right';
+
+export type TouchHandler = (edge: EdgePosition) => any;
+
+export interface ScrollBoundaryProps
+    extends Partial<Record<EdgePosition, ReactNode>> {
+    className?: string;
+    onTouch: TouchHandler;
+}
+
+function touch(edge: EdgePosition, onTouch: TouchHandler) {
+    return (node: HTMLElement | null) =>
+        node &&
+        new IntersectionObserver(
+            ([{ isIntersecting }]) => isIntersecting && onTouch(edge)
+        ).observe(node);
+}
+
+export const ScrollBoundary: FC<ScrollBoundaryProps> = ({
+    className,
+    onTouch,
+    top,
+    left,
+    right,
+    bottom,
+    children
+}) => (
+    <div className={classNames('position-relative', className)}>
+        <div
+            className="position-absolute top-0 left-0 w-100"
+            ref={touch('top', onTouch)}
+        >
+            {top}
+        </div>
+        <div
+            className="position-absolute top-0 left-0 h-100"
+            ref={touch('left', onTouch)}
+        >
+            {left}
+        </div>
+
+        {children}
+
+        <div
+            className="position-absolute top-0 right-0 h-100"
+            ref={touch('right', onTouch)}
+        >
+            {right}
+        </div>
+        <div
+            className="position-absolute top-100 left-0 w-100"
+            ref={touch('bottom', onTouch)}
+        >
+            {bottom}
+        </div>
+    </div>
+);
diff --git a/source/ScrollList.tsx b/source/ScrollList.tsx
new file mode 100644
index 0000000..0929012
--- /dev/null
+++ b/source/ScrollList.tsx
@@ -0,0 +1,69 @@
+import { debounce } from 'lodash';
+import { TranslationModel } from 'mobx-i18n';
+import { DataObject, NewData, ListModel, Stream } from 'mobx-restful';
+import { Component, ReactNode } from 'react';
+
+import { EdgePosition, ScrollBoundary } from './ScrollBoundary';
+
+export interface ScrollListProps<T extends DataObject = DataObject> {
+  defaultData?: T[];
+}
+
+export type DataType<P> = P extends ScrollListProps<infer D> ? D : never;
+
+export abstract class ScrollList<
+  P extends ScrollListProps,
+> extends Component<P> {
+  abstract store: ListModel<DataType<P>>;
+  abstract translater: TranslationModel<string, 'load_more' | 'no_more'>;
+
+  filter: NewData<DataType<P>> = {};
+
+  async boot() {
+    const BaseStream = Stream<DataObject>;
+
+    const store = this.store as unknown as InstanceType<
+        ReturnType<typeof BaseStream>
+      >,
+      { defaultData } = this.props,
+      { filter } = this;
+
+    if (store.downloading > 0) return;
+
+    store.clear();
+
+    if (defaultData) await store.restoreList({ allItems: defaultData, filter });
+
+    await store.getList(filter, store.pageList.length + 1);
+  }
+
+  componentWillUnmount() {
+    this.store.clear();
+  }
+
+  loadMore = debounce((edge: EdgePosition) => {
+    const { store } = this;
+
+    if (edge === 'bottom' && store.downloading < 1 && !store.noMore)
+      store.getList();
+  });
+
+  abstract renderList(): ReactNode;
+
+  render() {
+    const { t } = this.translater,
+      { noMore, allItems } = this.store;
+
+    return (
+      <ScrollBoundary onTouch={this.loadMore}>
+        <div>
+          {this.renderList()}
+
+          <footer className="mt-4 text-center text-muted small">
+            {noMore || !allItems.length ? t('no_more') : t('load_more')}
+          </footer>
+        </div>
+      </ScrollBoundary>
+    );
+  }
+}
diff --git a/source/index.ts b/source/index.ts
index a4d5284..63868ed 100644
--- a/source/index.ts
+++ b/source/index.ts
@@ -1,3 +1,5 @@
 export * from './Pager';
 export * from './FormField';
 export * from './RestTable';
+export * from './ScrollBoundary';
+export * from './ScrollList';
