Repository: mui/material-ui (TypeScript) — Feature

Issue:
[Table] Fixing head in material
<!-- Have a QUESTION? Please ask in [StackOverflow or gitter](http://tr.im/77pVj. -->


### For Fixing table Head in material 1.0.0 11 alpha 

I have a table with scrolling rows but I am not able to fix the header. 
Is there a property to do so as fixedHeader was in material 0.15 and up but there doesnt seem to be something similar in 1.0.0 version


### Versions

- Material-UI: 1.0.0-alpha 11
- React: 15.4.2



<!-- If you are having an issue with click events, please re-read the [README](http://tr.im/410Fg) (you did read the README, right? :-) ).

If you think you have found a _new_ issue that hasn't already been reported or fixed in HEAD, please complete the template above.

For feature requests, please delete the template above and use this one instead:

### Description
### Images & references

-->



Discussion:
I ran into the same issue today. Version 0.15 had the `fixedHeader` prop, v 1.x doesn't. I assume that there are plans to get this in.
Same problem with `Material-ui v1.0.0-beta.16`.
Would be really nice to have this feature back!
Same problem with Material-ui v1.0.0-beta.33

This is a must have feature! I think anyone who works with data grids would agree with me.
@mariorubinas If this feature is important to you, please consider submitting a PR. You can use this issue if you'd like to discuss your approach.
This is a must have feature!! feature worked great in the old MUI version. 
@asrane Looking forward to your pull request.
Hey @mbrookes I use the fixed header enough and like everything else about 1.0 enough that I'm going to take a stab at this — do you/the Material-UI organization have a preferred way of fixing table headers? The old `fixedHeader` was great, but I wasn't the biggest fan of how it made all the columns equal width (`table-layout: fixed;`), so I was going to try to re-implement it to support dynamic column widths.
@tambling That would be great! support for dynamic column widths would be ideal.
To be noted, the header can be sticky positioned:
```js
// ...

  head: {
    backgroundColor: "#fff",
    position: "sticky",
    top: 0
  }

// ...

        <TableHead>
          <TableRow>
            <TableCell className={classes.head}>

// ...
```
https://codesandbox.io/s/k0vwm7xpl3
@oliviertassinari Thanks, It works.
@oliviertassinari  Buttons in the table cell are overlaying the table header. How to resolve it ?
https://codesandbox.io/s/qx24l9vrz6

@pranayyelugam You will need to adjust the zIndex for your buttons.
@mbrookes  What should be the zIndex value for the buttons to appear and function properly ?
Here's the demo
https://codesandbox.io/s/qx24l9vrz6
@pranayyelugam you can easily set the z-index on the sticky header and it will have the expected result you want. 

```
head: {
    backgroundColor: "#fff",
    position: "sticky",
    top: 0,
    zIndex: 10,
  }
```
For benchmarking purposes: https://vuematerial.io/components/table.
@oliviertassinari, that won't work on IE 11 though, right? Technically Material UI supports IE 11. Is there a solution that will work with IE 11?
Does anyone have a solution for fixing the header of a table inside a `<Dialog>` component? `position: sticky` is not working.
Doesn't seem to work on Chrome
I have a fixed header in this component: https://github.com/SurLaTable/slt-ui/blob/develop/src/ComparisonChart/ComparisonTable/ComparisonTable.js

Material UI 3.x.x!
Still no fixed header? IMO its important feature..)
Bump!
Any idea about this? I'm using Material Table, but I'm not able to fix the header when the scroll shows up.
Just use two tables like in my example, @mhidalgop.
> Just use two tables like in my example, @mhidalgop.

Sorry, but it's not work for me :(
> Sorry, but it's not work for me :(

@mhidalgop, mind sharing your example in something like CodeSandbox?
Finally I have resolved my problem using this solution:
```
head: {
    backgroundColor: "#fff",
    position: "sticky",
    top: 0,
    zIndex: 10,
  }
```
Thxs!
:)
> To be noted, the header can be sticky positioned:
> 
> ```js
> // ...
> 
>   head: {
>     backgroundColor: "#fff",
>     position: "sticky",
>     top: 0
>   }
> 
> // ...
> 
>         <TableHead>
>           <TableRow>
>             <TableCell className={classes.head}>
> 
> // ...
> ```
> 
> https://codesandbox.io/s/k0vwm7xpl3

This works fine on a desktop browser, however, on mobile browsers it doesn't work.
> > To be noted, the header can be sticky positioned:
> > ```js
> > // ...
> > 
> >   head: {
> >     backgroundColor: "#fff",
> >     position: "sticky",
> >     top: 0
> >   }
> > 
> > // ...
> > 
> >         <TableHead>
> >           <TableRow>
> >             <TableCell className={classes.head}>
> > 
> > // ...
> > ```
> > 
> > 
> > https://codesandbox.io/s/k0vwm7xpl3
> 
> This works fine on a desktop browser, however, on mobile browsers it doesn't work.

I'm using this solution in a web app, and in a tablet with Chrome it works fine.
the sticky does not work for me, so any other solution on how to fix the table header?

Resolving patch:
diff --git a/docs/pages/api/table-cell.md b/docs/pages/api/table-cell.md
--- a/docs/pages/api/table-cell.md
+++ b/docs/pages/api/table-cell.md
@@ -56,6 +56,7 @@ Any other props supplied will be provided to the root element (native element).
 | <span class="prop-name">alignCenter</span> | <span class="prop-name">MuiTableCell-alignCenter</span> | Styles applied to the root element if `align="center"`.
 | <span class="prop-name">alignRight</span> | <span class="prop-name">MuiTableCell-alignRight</span> | Styles applied to the root element if `align="right"`.
 | <span class="prop-name">alignJustify</span> | <span class="prop-name">MuiTableCell-alignJustify</span> | Styles applied to the root element if `align="justify"`.
+| <span class="prop-name">stickyHeader</span> | <span class="prop-name">MuiTableCell-stickyHeader</span> | Styles applied to the root element if `context.table.stickyHeader={true}`.
 
 You can override the style of the component thanks to one of these customization points:
 
diff --git a/docs/pages/api/table.md b/docs/pages/api/table.md
--- a/docs/pages/api/table.md
+++ b/docs/pages/api/table.md
@@ -29,6 +29,7 @@ You can learn more about the difference by [reading our guide](/guides/minimizin
 | <span class="prop-name">component</span> | <span class="prop-type">elementType</span> | <span class="prop-default">'table'</span> | The component used for the root node. Either a string to use a DOM element or a component. |
 | <span class="prop-name">padding</span> | <span class="prop-type">'default'<br>&#124;&nbsp;'checkbox'<br>&#124;&nbsp;'none'</span> | <span class="prop-default">'default'</span> | Allows TableCells to inherit padding of the Table. |
 | <span class="prop-name">size</span> | <span class="prop-type">'small'<br>&#124;&nbsp;'medium'</span> | <span class="prop-default">'medium'</span> | Allows TableCells to inherit size of the Table. |
+| <span class="prop-name">stickyHeader</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | Set the header sticky.<br>⚠️ It doesn't work with IE 11. |
 
 The `ref` is forwarded to the root element.
 
@@ -42,6 +43,7 @@ Any other props supplied will be provided to the root element (native element).
 | Rule name | Global class | Description |
 |:-----|:-------------|:------------|
 | <span class="prop-name">root</span> | <span class="prop-name">MuiTable-root</span> | Styles applied to the root element.
+| <span class="prop-name">stickyHeader</span> | <span class="prop-name">MuiTable-stickyHeader</span> | Styles applied to the root element if `stickyHeader={true}`.
 
 You can override the style of the component thanks to one of these customization points:
 
diff --git a/docs/src/pages/components/tables/StickyHeadTable.js b/docs/src/pages/components/tables/StickyHeadTable.js
new file mode 100644
--- /dev/null
+++ b/docs/src/pages/components/tables/StickyHeadTable.js
@@ -0,0 +1,136 @@
+import React from 'react';
+import { makeStyles } from '@material-ui/core/styles';
+import Paper from '@material-ui/core/Paper';
+import Table from '@material-ui/core/Table';
+import TableBody from '@material-ui/core/TableBody';
+import TableCell from '@material-ui/core/TableCell';
+import TableHead from '@material-ui/core/TableHead';
+import TablePagination from '@material-ui/core/TablePagination';
+import TableRow from '@material-ui/core/TableRow';
+
+const columns = [
+  { id: 'name', label: 'Name', minWidth: 200 },
+  { id: 'code', label: 'ISO\u00a0Code', minWidth: 100 },
+  {
+    id: 'population',
+    label: 'Population',
+    minWidth: 120,
+    align: 'right',
+    format: value => value.toLocaleString(),
+  },
+  {
+    id: 'size',
+    label: 'Size\u00a0(km\u00b2)',
+    minWidth: 120,
+    align: 'right',
+    format: value => value.toLocaleString(),
+  },
+  {
+    id: 'density',
+    label: 'Density',
+    minWidth: 120,
+    align: 'right',
+    format: value => value.toFixed(2),
+  },
+];
+
+function createData(name, code, population, size) {
+  const density = population / size;
+  return { name, code, population, size, density };
+}
+
+const rows = [
+  createData('India', 'IN', 1324171354, 3287263),
+  createData('China', 'CN', 1403500365, 9596961),
+  createData('Italy', 'IT', 60483973, 301340),
+  createData('United States', 'US', 327167434, 9833520),
+  createData('Canada', 'CA', 37602103, 9984670),
+  createData('Australia', 'AU', 25475400, 7692024),
+  createData('Germany', 'DE', 83019200, 357578),
+  createData('Ireland', 'IE', 4857000, 70273),
+  createData('Mexico', 'MX', 126577691, 1972550),
+  createData('Japan', 'JP', 126317000, 377973),
+  createData('France', 'FR', 67022000, 640679),
+  createData('United Kingdom', 'GB', 67545757, 242495),
+  createData('Russia', 'RU', 146793744, 17098246),
+  createData('Nigeria', 'NG', 200962417, 923768),
+  createData('Brazil', 'BR', 210147125, 8515767),
+];
+
+const useStyles = makeStyles({
+  root: {
+    width: '100%',
+  },
+  tableWrapper: {
+    maxHeight: 407,
+    overflow: 'auto',
+  },
+});
+
+export default function StickyHeadTable() {
+  const classes = useStyles();
+  const [page, setPage] = React.useState(0);
+  const [rowsPerPage, setRowsPerPage] = React.useState(10);
+
+  function handleChangePage(event, newPage) {
+    setPage(newPage);
+  }
+
+  function handleChangeRowsPerPage(event) {
+    setRowsPerPage(+event.target.value);
+    setPage(0);
+  }
+
+  return (
+    <Paper className={classes.root}>
+      <div className={classes.tableWrapper}>
+        <Table stickyHeader>
+          <TableHead>
+            <TableRow>
+              {columns.map(column => (
+                <TableCell
+                  key={column.id}
+                  align={column.align}
+                  style={{ minWidth: column.minWidth }}
+                >
+                  {column.label}
+                </TableCell>
+              ))}
+            </TableRow>
+          </TableHead>
+          <TableBody>
+            {rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map(row => {
+              return (
+                <TableRow hover role="checkbox" tabIndex={-1} key={row.code}>
+                  {columns.map(column => {
+                    const value = row[column.id];
+                    return (
+                      <TableCell key={column.id} align={column.align}>
+                        {column.format && typeof value === 'number' ? column.format(value) : value}
+                      </TableCell>
+                    );
+                  })}
+                </TableRow>
+              );
+            })}
+          </TableBody>
+        </Table>
+      </div>
+      <TablePagination
+        rowsPerPageOptions={[10, 25, 100]}
+        component="div"
+        count={rows.length}
+        rowsPerPage={rowsPerPage}
+        page={page}
+        backIconButtonProps={{
+          'aria-label': 'previous page',
+        }}
+        nextIconButtonProps={{
+          'aria-label': 'next page',
+        }}
+        onChangePage={handleChangePage}
+        onChangeRowsPerPage={handleChangeRowsPerPage}
+      />
+    </Paper>
+  );
+}
diff --git a/docs/src/pages/components/tables/StickyHeadTable.tsx b/docs/src/pages/components/tables/StickyHeadTable.tsx
new file mode 100644
--- /dev/null
+++ b/docs/src/pages/components/tables/StickyHeadTable.tsx
@@ -0,0 +1,152 @@
+import React from 'react';
+import { makeStyles } from '@material-ui/core/styles';
+import Paper from '@material-ui/core/Paper';
+import Table from '@material-ui/core/Table';
+import TableBody from '@material-ui/core/TableBody';
+import TableCell from '@material-ui/core/TableCell';
+import TableHead from '@material-ui/core/TableHead';
+import TablePagination from '@material-ui/core/TablePagination';
+import TableRow from '@material-ui/core/TableRow';
+
+interface Column {
+  id: 'name' | 'code' | 'population' | 'size' | 'density';
+  label: string;
+  minWidth?: number;
+  align?: 'right';
+  format?: (value: number) => string;
+}
+
+const columns: Column[] = [
+  { id: 'name', label: 'Name', minWidth: 200 },
+  { id: 'code', label: 'ISO\u00a0Code', minWidth: 100 },
+  {
+    id: 'population',
+    label: 'Population',
+    minWidth: 120,
+    align: 'right',
+    format: (value: number) => value.toLocaleString(),
+  },
+  {
+    id: 'size',
+    label: 'Size\u00a0(km\u00b2)',
+    minWidth: 120,
+    align: 'right',
+    format: (value: number) => value.toLocaleString(),
+  },
+  {
+    id: 'density',
+    label: 'Density',
+    minWidth: 120,
+    align: 'right',
+    format: (value: number) => value.toFixed(2),
+  },
+];
+
+interface Data {
+  name: string;
+  code: string;
+  population: number;
+  size: number;
+  density: number;
+}
+
+function createData(name: string, code: string, population: number, size: number): Data {
+  const density = population / size;
+  return { name, code, population, size, density };
+}
+
+const rows = [
+  createData('India', 'IN', 1324171354, 3287263),
+  createData('China', 'CN', 1403500365, 9596961),
+  createData('Italy', 'IT', 60483973, 301340),
+  createData('United States', 'US', 327167434, 9833520),
+  createData('Canada', 'CA', 37602103, 9984670),
+  createData('Australia', 'AU', 25475400, 7692024),
+  createData('Germany', 'DE', 83019200, 357578),
+  createData('Ireland', 'IE', 4857000, 70273),
+  createData('Mexico', 'MX', 126577691, 1972550),
+  createData('Japan', 'JP', 126317000, 377973),
+  createData('France', 'FR', 67022000, 640679),
+  createData('United Kingdom', 'GB', 67545757, 242495),
+  createData('Russia', 'RU', 146793744, 17098246),
+  createData('Nigeria', 'NG', 200962417, 923768),
+  createData('Brazil', 'BR', 210147125, 8515767),
+];
+
+const useStyles = makeStyles({
+  root: {
+    width: '100%',
+  },
+  tableWrapper: {
+    maxHeight: 407,
+    overflow: 'auto',
+  },
+});
+
+export default function StickyHeadTable() {
+  const classes = useStyles();
+  const [page, setPage] = React.useState(0);
+  const [rowsPerPage, setRowsPerPage] = React.useState(10);
+
+  function handleChangePage(event: unknown, newPage: number) {
+    setPage(newPage);
+  }
+
+  function handleChangeRowsPerPage(event: React.ChangeEvent<HTMLInputElement>) {
+    setRowsPerPage(+event.target.value);
+    setPage(0);
+  }
+
+  return (
+    <Paper className={classes.root}>
+      <div className={classes.tableWrapper}>
+        <Table stickyHeader>
+          <TableHead>
+            <TableRow>
+              {columns.map(column => (
+                <TableCell
+                  key={column.id}
+                  align={column.align}
+                  style={{ minWidth: column.minWidth }}
+                >
+                  {column.label}
+                </TableCell>
+              ))}
+            </TableRow>
+          </TableHead>
+          <TableBody>
+            {rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map(row => {
+              return (
+                <TableRow hover role="checkbox" tabIndex={-1} key={row.code}>
+                  {columns.map(column => {
+                    const value = row[column.id];
+                    return (
+                      <TableCell key={column.id} align={column.align}>
+                        {column.format && typeof value === 'number' ? column.format(value) : value}
+                      </TableCell>
+                    );
+                  })}
+                </TableRow>
+              );
+            })}
+          </TableBody>
+        </Table>
+      </div>
+      <TablePagination
+        rowsPerPageOptions={[10, 25, 100]}
+        component="div"
+        count={rows.length}
+        rowsPerPage={rowsPerPage}
+        page={page}
+        backIconButtonProps={{
+          'aria-label': 'previous page',
+        }}
+        nextIconButtonProps={{
+          'aria-label': 'next page',
+        }}
+        onChangePage={handleChangePage}
+        onChangeRowsPerPage={handleChangeRowsPerPage}
+      />
+    </Paper>
+  );
+}
diff --git a/docs/src/pages/components/tables/tables.md b/docs/src/pages/components/tables/tables.md
--- a/docs/src/pages/components/tables/tables.md
+++ b/docs/src/pages/components/tables/tables.md
@@ -59,6 +59,13 @@ custom actions.
 
 {{"demo": "pages/components/tables/CustomPaginationActionsTable.js"}}
 
+## Fixed header
+
+An example of a table with scrollable rows and fixed column headers.
+It leverages the `stickyHeader` prop (⚠️ no IE 11 support).
+
+{{"demo": "pages/components/tables/StickyHeadTable.js"}}
+
 ## Spanning Table
 
 A simple example with spanning rows & columns.
diff --git a/packages/material-ui/src/Table/Table.d.ts b/packages/material-ui/src/Table/Table.d.ts
--- a/packages/material-ui/src/Table/Table.d.ts
+++ b/packages/material-ui/src/Table/Table.d.ts
@@ -5,6 +5,7 @@ export interface TableProps extends StandardProps<TableBaseProps, TableClassKey>
   component?: React.ElementType<TableBaseProps>;
   padding?: Padding;
   size?: Size;
+  stickyHeader?: boolean;
 }
 
 export type TableBaseProps = React.TableHTMLAttributes<HTMLTableElement>;
@@ -13,7 +14,7 @@ export type Padding = 'default' | 'checkbox' | 'none';
 
 export type Size = 'small' | 'medium';
 
-export type TableClassKey = 'root';
+export type TableClassKey = 'root' | 'stickyHeader';
 
 declare const Table: React.ComponentType<TableProps>;
 
diff --git a/packages/material-ui/src/Table/Table.js b/packages/material-ui/src/Table/Table.js
--- a/packages/material-ui/src/Table/Table.js
+++ b/packages/material-ui/src/Table/Table.js
@@ -12,6 +12,10 @@ export const styles = {
     borderCollapse: 'collapse',
     borderSpacing: 0,
   },
+  /* Styles applied to the root element if `stickyHeader={true}`. */
+  stickyHeader: {
+    borderCollapse: 'separate',
+  },
 };
 
 const Table = React.forwardRef(function Table(props, ref) {
@@ -21,13 +25,22 @@ const Table = React.forwardRef(function Table(props, ref) {
     component: Component = 'table',
     padding = 'default',
     size = 'medium',
+    stickyHeader = false,
     ...other
   } = props;
-  const table = React.useMemo(() => ({ padding, size }), [padding, size]);
+  const table = React.useMemo(() => ({ padding, size, stickyHeader }), [
+    padding,
+    size,
+    stickyHeader,
+  ]);
 
   return (
     <TableContext.Provider value={table}>
-      <Component ref={ref} className={clsx(classes.root, className)} {...other} />
+      <Component
+        ref={ref}
+        className={clsx(classes.root, { [classes.stickyHeader]: stickyHeader }, className)}
+        {...other}
+      />
     </TableContext.Provider>
   );
 });
@@ -59,6 +72,12 @@ Table.propTypes = {
    * Allows TableCells to inherit size of the Table.
    */
   size: PropTypes.oneOf(['small', 'medium']),
+  /**
+   * Set the header sticky.
+   *
+   * ⚠️ It doesn't work with IE 11.
+   */
+  stickyHeader: PropTypes.bool,
 };
 
 export default withStyles(styles, { name: 'MuiTable' })(Table);
diff --git a/packages/material-ui/src/TableCell/TableCell.d.ts b/packages/material-ui/src/TableCell/TableCell.d.ts
--- a/packages/material-ui/src/TableCell/TableCell.d.ts
+++ b/packages/material-ui/src/TableCell/TableCell.d.ts
@@ -39,7 +39,8 @@ export type TableCellClassKey =
   | 'alignJustify'
   | 'sizeSmall'
   | 'paddingCheckbox'
-  | 'paddingNone';
+  | 'paddingNone'
+  | 'stickyHeader';
 
 declare const TableCell: React.ComponentType<TableCellProps>;
 
diff --git a/packages/material-ui/src/TableCell/TableCell.js b/packages/material-ui/src/TableCell/TableCell.js
--- a/packages/material-ui/src/TableCell/TableCell.js
+++ b/packages/material-ui/src/TableCell/TableCell.js
@@ -96,6 +96,13 @@ export const styles = theme => ({
   alignJustify: {
     textAlign: 'justify',
   },
+  /* Styles applied to the root element if `context.table.stickyHeader={true}`. */
+  stickyHeader: {
+    position: 'sticky',
+    top: 0,
+    left: 0,
+    backgroundColor: theme.palette.background.default,
+  },
 });
 
 const TableCell = React.forwardRef(function TableCell(props, ref) {
@@ -108,7 +115,7 @@ const TableCell = React.forwardRef(function TableCell(props, ref) {
     scope: scopeProp,
     size: sizeProp,
     sortDirection,
-    variant,
+    variant: variantProp,
     ...other
   } = props;
 
@@ -128,6 +135,7 @@ const TableCell = React.forwardRef(function TableCell(props, ref) {
   }
   const padding = paddingProp || (table && table.padding ? table.padding : 'default');
   const size = sizeProp || (table && table.size ? table.size : 'medium');
+  const variant = variantProp || (tablelvl2 && tablelvl2.variant);
 
   let ariaSort = null;
   if (sortDirection) {
@@ -140,11 +148,10 @@ const TableCell = React.forwardRef(function TableCell(props, ref) {
       className={clsx(
         classes.root,
         {
-          [classes.head]: variant ? variant === 'head' : tablelvl2 && tablelvl2.variant === 'head',
-          [classes.body]: variant ? variant === 'body' : tablelvl2 && tablelvl2.variant === 'body',
-          [classes.footer]: variant
-            ? variant === 'footer'
-            : tablelvl2 && tablelvl2.variant === 'footer',
+          [classes.head]: variant === 'head',
+          [classes.stickyHeader]: variant === 'head' && table && table.stickyHeader,
+          [classes.body]: variant === 'body',
+          [classes.footer]: variant === 'footer',
           [classes[`align${capitalize(align)}`]]: align !== 'inherit',
           [classes[`padding${capitalize(padding)}`]]: padding !== 'default',
           [classes[`size${capitalize(size)}`]]: size !== 'medium',


Test patch:
diff --git a/packages/material-ui/src/Table/Table.test.js b/packages/material-ui/src/Table/Table.test.js
--- a/packages/material-ui/src/Table/Table.test.js
+++ b/packages/material-ui/src/Table/Table.test.js
@@ -70,6 +70,7 @@ describe('<Table />', () => {
     expect(context).to.deep.equal({
       size: 'medium',
       padding: 'default',
+      stickyHeader: false,
     });
   });
 });