Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using it's URL and the proper URL extention.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                
              
            
!

CSS

              
                $color-blue: #3a77ff;
$color-dark-blue: #28375a;
$row-height: 40px;
$table-gap: 1px;

html, body, .app {
  width: 100%;
  height: 100%;
  font-family: 'Montserrat', sans-serif;
}

.app {
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: rgba($color-blue, 0.2);
}

.table {
  background-color: rgba($color-blue, 0.2);
  border: 1px solid rgba($color-blue, 0.5);
  max-height: 80%;
  border-radius: 4px;
  box-shadow: 0 0 5px rgba($color-blue, 0.4), 0 0 25px rgba($color-blue, 0.2), 0 0 200px 150px white;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  
  .header {
    display: grid;
    grid-template-columns: 70px repeat(var(--columns), 90px);
    grid-gap: $table-gap;
    border-bottom: 1px solid rgba($color-blue, 0.2);
    box-shadow: 0 0 2px 2px rgba($color-blue, 0.15), 0 0 15px 5px rgba($color-blue, 0.15);
    z-index: 1;
    font-weight: bold;
    
    .table-cell {
      background-color: mix($color-blue, white, 20%);
    }
  }
  
  .table-inner {
    display: grid;
    grid-template-columns: 70px repeat(var(--columns), 90px);
    grid-gap: $table-gap;
    height: 100%;
    overflow: auto;
  }
  
  .table-cell {
    padding: 0 15px;
    background-color: white;
    display: flex;
    align-items: center;
    justify-content: center;
    height: $row-height;
    color: $color-dark-blue;
    
    &.column-0 {
      padding: 0 10px;
    }
    
    &.optimistic {
      position: relative;
      background-image: linear-gradient(rgba(58, 119, 255, 0.2) 1px, transparent 1px);
      background-size: $row-height+$table-gap $row-height+$table-gap;
      background-repeat: repeat;
      margin-top: -1px; // This fixes the double gab below the visible rows that occurs as a result of the gradient meeting the grid gap.
      
      &::after {
        content: '';
        position: absolute;
        width: 100%;
        height: 100%;
        top: 0;
        left: 0;
        // background-size: 80px 80px;
        animation: blink 2s infinite;
        background-repeat: repeat;
        background-image: linear-gradient(to right, transparent 0, transparent 10%, rgba(58, 119, 255, 0.1) 10%, rgba(58, 119, 255, 0.1) 90%, transparent 90%);
      }
    }
  }
}

@keyframes blink {
  0%, 100% {
    opacity: 0;
  }
  50% {
    opacity: 1;
  }
}
              
            
!

JS

              
                // This is a proof of concept aimed at solving the following issues:
// 1. The CSS-Grid 1000 row limit 
//    (see https://stackoverflow.com/a/47345676/1096470)
// 2. Lazy data loading - rendering large tables where only a portion of the 
//    data is available at the client side
// 3. Performance issues that naturally occur when rendering very
//    large tables
// 
// The idea here is to only render the rows that are currently 
// visible to the user based on the scroll position.
// The non-visible rows are replaced with a single row that receives
// their total height (this row also receives a gradient background that
// simulates the horizontal row gap to make it look as if the lines are there).
//
// In terms of performance, a table of 100 rows will perform exactly 
// the same as a table with 10K rows.

const ROW_HEIGHT = 40;
const GAP_SIZE = 1;
const ROWS = 10000;
const COLS = 6;
const THRESHOLD = 300; // Represents the delay (in milliseconds) between data updates
const DATA_PADDING = 3; // Represents how much extra data should be rendered before
                        // and after the visibile rows to avoid showing empty rows 
                        // when scrolling. This number is multiplied by the number
                        // of visible rows.

const OptimisticRow = ({rows}) => (
  <React.Fragment>
    {[...new Array(COLS)].map((_, i) => (
      <div key={i} className='table-cell optimistic' style={{height: rows *(ROW_HEIGHT + GAP_SIZE)}}/>
    ))}
  </React.Fragment>
);

const TableCell = ({children, index}) => (
  <div 
    className={`table-cell column-${index}`}>
    {children}
  </div>
);

const TableRow = ({children}) => (
  children
);

class Table extends React.PureComponent {
  
  table = React.createRef();
  state = {from: 0, to: 30};
  previousScrolTop = 0;
  
  getSnapshotBeforeUpdate() {
    return this.table.current.scrollTop;
  }

  componentDidUpdate(prevProps, prevState, scrollTop) {
    // When the visible rows are moved down by changing 
    // the height of the optimistic row above them, the browser automatically
    // scrolls them back into view, which in turn creates another render
    // becuase the onScroll is called, resulting in an infinite loop.
    // To solve this, we get the snapshot before the DOM is updated
    // and check for a mismatch between the scrollTop before and after. 
    // If such a mismatch exists, it means that the scroll 
    // was done by the browser, and not the user, and therefore
    // we apply the scrollTop from the snapshot.
    if (scrollTop !== this.table.current.scrollTop) {
      this.table.current.scrollTop = scrollTop;
    }
  }
  
  debounce = _.debounce((scrollTop, clientHeight) => {
    const maxVisibleRows = Math.ceil(clientHeight / (ROW_HEIGHT + GAP_SIZE));
    const from = Math.max(0, Math.floor(scrollTop / (ROW_HEIGHT + GAP_SIZE)) - maxVisibleRows * DATA_PADDING);
    const to = Math.min(this.props.rows, from + maxVisibleRows * (DATA_PADDING * 2 + 1));
    this.setState({from, to});
  }, THRESHOLD);
  
  handleOnScroll = e => {
    const {scrollTop, clientHeight} = e.target;
    this.debounce(scrollTop, clientHeight);
  };
  
  render() {
    const {children, rows} = this.props;
    const {from, to} = this.state;
    return (
      <div className='table' style={{'--columns': COLS - 1}}>
        <div className='header'>
          <TableRow>
            <TableCell index={0}>Index</TableCell>
            {[...new Array(COLS - 1)].map((_, i) => (
              <TableCell index={i + 1} header>Header</TableCell>
            ))}
          </TableRow>
        </div>
        <div className='table-inner' onScroll={this.handleOnScroll} ref={this.table}>
          {from > 0 &&
            <OptimisticRow rows={from}/>}
          {children(from, to)}
          {to < rows &&
            <OptimisticRow rows={rows - to}/>}
        </div>
      </div>
    );
  }
}

class App extends React.PureComponent {
  
  render() {
    return (
      <div className='app'>
        <Table rows={ROWS}>
          {(from, to) => (
            [...new Array(to - from)].map((_, i) => (
              <TableRow key={i} index={i}>
                <TableCell index={0}>{i + from}</TableCell>
                {[...new Array(COLS - 1)].map((_, i) => (
                  <TableCell index={i + 1} key={i}>data</TableCell>
                ))}
              </TableRow>
            ))
          )}
        </Table>
      </div>
    );
  }
}

ReactDOM.render(
  <App/>,
  document.body
);
              
            
!
999px

Console