<div id="container"></div>
thead th {
  padding: 0;
  background: red;
}
.end-buffer-area {
  z-index: -1;
  position: relative;
}
.header-column {
  height: auto !important;
  padding: 10px;
  box-sizing: border-box;
}
.stickyHeader .header-column {
  position: fixed;
  top: 0;
  background: inherit;
  animation: slideDown 200ms ease-in;
}

@keyframes slideDown {
  0% {
    transform: translateY(-50%);
  }
  100% {
    transform: translateY(0%);
  }
}
function StickyHeaderWrapper({
  children,
}) {
  const el = React.useRef(null);
  const startEl = React.useRef(null);
  const endEl = React.useRef(null);

  React.useEffect(() => {
    if (el.current) {
      const show = () => {
        if (!el.current) {
          return;
        }
        (el.current.querySelectorAll('.header-column') || []).forEach(
          col => {
            if (!col.parentElement) { return; }
            const { width, height } =
              col.parentElement.getBoundingClientRect() || {};
            col.style.width = col.parentElement.style.width = `${width}px`;
            col.style.height = col.parentElement.style.height = `${height}px`;
            `${width}px`;
          }
        );
        el.current.classList.add("stickyHeader");
      };
      const hide = () => {
        if (!el.current) {
          return;
        }
        el.current.classList.remove("stickyHeader");
      };
      if (startEl.current && endEl.current) {
        const thead = el.current.querySelectorAll('thead');
        const rows = el.current.querySelectorAll('tr');
        const theadHeight = (thead && thead[0].getBoundingClientRect() || {}).height || 0;
        const lastRowHeight = (rows && rows[rows.length - 1].getBoundingClientRect() || {}).height || 0;
        endEl.current.style.top = `-${theadHeight + lastRowHeight/2}px`;
        const states = new Map();
        const observer = new IntersectionObserver(
          entries => {
            entries.forEach(e => {
              states.set(e.target, e.boundingClientRect);
            });
            const { top } = states.get(startEl.current) || {};
            const { top: bottom } = states.get(endEl.current) || {};
            if (top < 0 && bottom > 0) {
              show();
            } else {
              hide();
            }
          },
          {
            threshold: [0],
          }
        );
        observer.observe(startEl.current);
        observer.observe(endEl.current);
      }
    }
  }, []);

  return (
    <div className="wrapper" ref={el}>
      <div ref={startEl} />
      {children}
      <div className="end-buffer-area" ref={endEl} />
    </div>
  );
}

const data = [
  {
    name: 'John Brown',
    age: 32,
    address: 'New York No. 1 Lake Park',
  },
  {
    name: 'Jim Green',
    age: 42,
    address: 'London No. 1 Lake Park',
  },
  {
    name: 'Joe Black',
    age: 32,
    address: 'Sidney No. 1 Lake Park',
  },
  {
    name: 'Jim Red',
    age: 32,
    address: 'London No. 2 Lake Park',
  },
  {
    name: 'John Brown',
    age: 32,
    address: 'New York No. 1 Lake Park',
  },
  {
    name: 'Jim Green',
    age: 42,
    address: 'London No. 1 Lake Park',
  },
  {
    name: 'Joe Black',
    age: 32,
    address: 'Sidney No. 1 Lake Park',
  },
  {
    name: 'Jim Red',
    age: 32,
    address: 'London No. 2 Lake Park',
  },
  {
    name: 'John Brown',
    age: 32,
    address: 'New York No. 1 Lake Park',
  },
  {
    name: 'Jim Green',
    age: 42,
    address: 'London No. 1 Lake Park',
  },
  {
    name: 'Joe Black',
    age: 32,
    address: 'Sidney No. 1 Lake Park',
  },
  {
    name: 'Jim Red',
    age: 32,
    address: 'London No. 2 Lake Park',
  },
  {
    name: 'John Brown',
    age: 32,
    address: 'New York No. 1 Lake Park',
  },
  {
    name: 'Jim Green',
    age: 42,
    address: 'London No. 1 Lake Park',
  },
  {
    name: 'Joe Black',
    age: 32,
    address: 'Sidney No. 1 Lake Park',
  },
  {
    name: 'Jim Red',
    age: 32,
    address: 'London No. 2 Lake Park',
  },
];

function Table() {
  return (
    <div>
      <h1>Sticky header wrapper</h1>
      <StickyHeaderWrapper>
        <table>
          <thead>
            <tr>
              <th><div className="header-column">Name</div></th>
              <th><div className="header-column">Age</div></th>
              <th><div className="header-column">Address</div></th>
            </tr>
          </thead>
          <tbody>
            {data.map((d, i) => (
              <tr key={i}>
                <td>{d.name}</td>
                <td>{d.age}</td>
                <td>{d.address}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </StickyHeaderWrapper>
      <div style={{ height: '300px' }}>Blank below</div>
    </div>
  );
}


ReactDOM.render(<Table />, document.getElementById('container'));
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://unpkg.com/[email protected]/umd/react.development.js
  2. https://unpkg.com/[email protected]/umd/react-dom.development.js