ui icon indicating copy to clipboard operation
ui copied to clipboard

Fixed table header, with scrollable body

Open jackmismash opened this issue 1 year ago • 21 comments

Hello, how can I fix the table header so I can scroll the body inside of a scroll area. Currently, this is what I have:

<ScrollArea className="m-4 h-[225px] w-[500px] relative rounded-md">
           <Table className="relative">
               <TableHeader className="bg-gray-500 fixed">
                   <TableRow className="bg-yellow-500 w-[500px]">
                       <TableHead>Name</TableHead>
                       <TableHead className="text-center">Actual/Expected</TableHead>
                       <TableHead>Slant Range</TableHead>
                   </TableRow>
               </TableHeader>
               <TableBody className="mt-10">
                   {Object.keys(frameRates).map((chipId) => (
                       <TableRow>
                           <TableCell>{chipId}</TableCell>
                           <TableCell className="text-center">{frameRates[chipId]}/6 fps</TableCell>
                           <TableCell>2000m</TableCell>
                       </TableRow>
                   ))}
               </TableBody>
           </Table>
       </ScrollArea>

which renders this: image I want the yellow area to be fixed at the top of the scroll area and I want the body to be scrollable. e.g. if I added another element, it would overflow so I can scroll down to see the newly added element while also being able to see that header since it will be fixed. I am open to suggestions as well! Thanks in advance.

jackmismash avatar Aug 08 '23 21:08 jackmismash

I've tried a few ways to solve this but no progress so far.

Wrapping the TableBody in a div with a height and overflow-y scroll stops the header from scrolling but the header info get's shifted to the right and no longer lines up with the columns. I suspect there's some assumptions that have been made to makes this harder than it seems.

Would be good to at least get some updates if there's an easy fix or this requires a major change.

dbroadhurst avatar Sep 18 '23 14:09 dbroadhurst

Hey guys,

I had the same struggle and I spent a couple hours yelling at my self. You can use the sticky property on the <TableHeader> element. But for this to work, you have to go into the table.tsx component in your @/components/ui folder and look for the definition of the <Table/> element. It's already wrapped in a div, causing the sticky effect not working. I just removed it, and it works!

Remember, sticky property goes with top: 0px; in order to work, and the parent container has to be relative as well.

Good luck with that!

BLR19 avatar Oct 04 '23 07:10 BLR19

Hey guys,

I had the same struggle and I spent a couple hours yelling at my self. You can use the sticky property on the <TableHeader> element. But for this to work, you have to go into the table.tsx component in your @/components/ui folder and look for the definition of the <Table/> element. It's already wrapped in a div, causing the sticky effect not working. I just removed it, and it works!

Remember, sticky property goes with top: 0px; in order to work, and the parent container has to be relative as well.

Good luck with that!

Tried but the borders seem to still scroll. Any fixes for that? @BLR19

Husain010 avatar Oct 06 '23 09:10 Husain010

Our 2 developers tried but no luck. If someone already fixed this issue please respond. Thank you.

shahreazebd avatar Oct 16 '23 18:10 shahreazebd

I have made a partial workaround:

On the instantiated table.tsx file I have created an Interface called ExtendedTableProps, like so:

interface ExtendedTableProps extends React.HTMLAttributes<HTMLTableElement> {
  divClassname?: string;
}

this way, I can pass a divClassname override classes when I want to make the table scrollable:

const Table = React.forwardRef<HTMLTableElement, ExtendedTableProps>(
  ({ className, divClassname, ...props }, ref) => (
    <div className={cn("relative w-full overflow-auto", divClassname)}>
      <table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} />
    </div>
  )
);

I can then make the height fixed without it behind mandatory:

 <Table
          className="rounded-md border-border w-full h-10 overflow-clip relative"
          divClassname="max-h-screen overflow-y-scroll"
        >
 {....table code}
</Table>

And then, finally pass the sticky props to the <TableHeader> component.

<TableHeader className="sticky w-full top-0 h-10 border-b-2 border-border rounded-t-md  bg-stone-700">
....
</TableHeader>

The issue with this implementation is that the table loses its rounded corners when you scroll it down. I'm still investivating the reason why, the functionality is mainly here.

tverderesi avatar Oct 17 '23 21:10 tverderesi

Same issue, different solution to @tverderesi. I just removed the wrapping div from Table. Not sure why it is wrapped by div <div className="relative w-full overflow-auto">

mununki avatar Oct 18 '23 08:10 mununki

The reason thead is still scrolled even though you add sticky, top-0 into thead, because there is hiddent wrapping div as a parent of table.

mununki avatar Oct 18 '23 08:10 mununki

Hey guys,

I had the same struggle and I spent a couple hours yelling at my self. You can use the sticky property on the <TableHeader> element. But for this to work, you have to go into the table.tsx component in your @/components/ui folder and look for the definition of the <Table/> element. It's already wrapped in a div, causing the sticky effect not working. I just removed it, and it works!

Remember, sticky property goes with top: 0px; in order to work, and the parent container has to be relative as well.

Good luck with that!

@mununki This already explained here

BLR19 avatar Oct 18 '23 08:10 BLR19

Hey guys, I had the same struggle and I spent a couple hours yelling at my self. You can use the sticky property on the <TableHeader> element. But for this to work, you have to go into the table.tsx component in your @/components/ui folder and look for the definition of the <Table/> element. It's already wrapped in a div, causing the sticky effect not working. I just removed it, and it works! Remember, sticky property goes with top: 0px; in order to work, and the parent container has to be relative as well. Good luck with that!

Tried but the borders seem to still scroll. Any fixes for that? @BLR19

Same problem. If anyone has a solution, I'm interested !

MaximeLeBerre avatar Oct 31 '23 15:10 MaximeLeBerre

I have identified a solution:

  • Start by removing the div code from the table.

    image

  • Place a div above the table component, setting its height, and relative, and add the 'overflow-auto' class to it.

  • In the TableHeader component, apply a 'sticky' class with a 'top-0' property.

    image

By following these steps, you can resolve the issue of the fixed table header

brijeshkumar16 avatar Nov 07 '23 06:11 brijeshkumar16

sticky property never works when one of the parents has overflow css property set, whatever value it is (auto, scroll... whatever). So the only solution is to remove that div the other guys has posted above since it has overflow-auto tailwind class or just remove that class if you need the div for the other classes it has (relative and w-full).

IlirEdis avatar Nov 10 '23 13:11 IlirEdis

@brijeshkumar16's answer works great.

Similarly you can also use the ScrollArea component if you want to be consistent with shadcn.

  1. Delete the div in Table
const Table = React.forwardRef<
  HTMLTableElement,
  React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
  <table
    ref={ref}
    className={cn("w-full caption-bottom text-sm", className)}
    {...props}
  />
));
Table.displayName = "Table";

Add sticky top-0 bg-secondary to TableHeader

const TableHeader = React.forwardRef<
  HTMLTableSectionElement,
  React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
  <thead
    ref={ref}
    // Manually added sticky top-0 to fix header not sticking to top of table
    className={cn("sticky top-0 bg-secondary [&_tr]:border-b", className)}
    {...props}
  />
));
TableHeader.displayName = "TableHeader";

Wrap your Table with ScrollArea and set a height

<ScrollArea className="h-[200px] rounded-md border">
 <Table>
 </Table>
</ScrollArea>

image

lmurdock12 avatar Nov 12 '23 03:11 lmurdock12

Hey guys,

I had the same struggle and I spent a couple hours yelling at my self. You can use the sticky property on the <TableHeader> element. But for this to work, you have to go into the table.tsx component in your @/components/ui folder and look for the definition of the <Table/> element. It's already wrapped in a div, causing the sticky effect not working. I just removed it, and it works!

Remember, sticky property goes with top: 0px; in order to work, and the parent container has to be relative as well.

Good luck with that!

This worked for me, thanks for the suggestion!

cysidron1 avatar Nov 23 '23 02:11 cysidron1

@brijeshkumar16's answer works great.

Similarly you can also use the ScrollArea component if you want to be consistent with shadcn.

  1. Delete the div in Table
const Table = React.forwardRef<
  HTMLTableElement,
  React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
  <table
    ref={ref}
    className={cn("w-full caption-bottom text-sm", className)}
    {...props}
  />
));
Table.displayName = "Table";

Add sticky top-0 bg-secondary to TableHeader

const TableHeader = React.forwardRef<
  HTMLTableSectionElement,
  React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
  <thead
    ref={ref}
    // Manually added sticky top-0 to fix header not sticking to top of table
    className={cn("sticky top-0 bg-secondary [&_tr]:border-b", className)}
    {...props}
  />
));
TableHeader.displayName = "TableHeader";

Wrap your Table with ScrollArea and set a height

<ScrollArea className="h-[200px] rounded-md border">
 <Table>
 </Table>
</ScrollArea>

image

This is great solution, thanks

hadi-alnaqash avatar Dec 06 '23 13:12 hadi-alnaqash

image

Thank you very much do you also know by any means how i can remove the scrolling from the header part cause this looks really ugly... I struggled with that for hours now and i havent found a solution

Xentox-Phil avatar Dec 08 '23 21:12 Xentox-Phil

You can remove the overflow-auto classname from the wrapper of the table from the table component inside the shadcn provided ui component and provide sticky top-0 classed to the tableHeader.

abhishek61067 avatar Dec 31 '23 05:12 abhishek61067

@lmurdock12 answer works great only in big viewports, its not fully responsive. @brijeshkumar16 worked perfectly in all screen sizes

JuanPedroPontVerges avatar Jan 01 '24 14:01 JuanPedroPontVerges

is there any way to achieve this without setting a static height ?

ypanagidis avatar Jan 03 '24 18:01 ypanagidis

@lmurdock12 answer works great only in big viewports, its not fully responsive. @brijeshkumar16 worked perfectly in all screen sizes

Try this.

import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'

<ScrollArea className="h-[400px] rounded-md border overflow-auto">
  // code here
  <TableRow key={headerGroup.id} className="sticky top-0 bg-secondary hover:bg-secondary">
  // code here
  <ScrollBar orientation="horizontal" />
</ScrollArea>

table.jsx (comment table div)

const Table = React.forwardRef(({ className, ...props }, ref) => (
  // <div className="relative w-full overflow-auto">
    <table
      ref={ref}
      className={cn("w-full caption-bottom text-sm", className)}
      {...props} />
  // </div>
))

gdineruiz avatar Jan 04 '24 06:01 gdineruiz

is there any way to achieve this without setting a static height ?

I've managed to not need to set a static height using css grid on the parent and max content and a max height set on the scroll area

<Table>
 <TableHeader className="sticky top-0 bg-secondary">
.....
</TableHeader>
  <TableBody className="grid auto-rows-max">
   <ScrollArea className="max-h-[200px]">
              ....
     </ScrollArea>
  </TableBody>
</Table>

This trick using css grid seems to work regularly for me with ScrollArea

Georgegriff avatar Jan 12 '24 22:01 Georgegriff

You can remove the overflow-auto classname from the wrapper of the table from the table component inside the shadcn provided ui component and provide sticky top-0 classed to the tableHeader.

i already have this but i still have the problem that the scroller is within the thead and i cant apply block overflow-auto and a fixed height on the TableBody since then the layout of the table is all over the place. If somebody can help me out I would really appreciate it!

image

Xentox-Phil avatar Jan 17 '24 20:01 Xentox-Phil

Fixed header overflow. Still need to set height.


const ScrollArea = React.forwardRef<
  React.ElementRef<typeof ScrollAreaPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> & {
    disableScrollbar?: boolean
  }
>(({ className, children, disableScrollbar, ...props }, ref) => (
  <ScrollAreaPrimitive.Root
    ref={ref}
    className={cn('relative overflow-hidden', className)}
    {...props}
  >
    <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
      {children}
    </ScrollAreaPrimitive.Viewport>
    {!disableScrollbar && <ScrollBar />} // need to remove default scrollbar, it overrides table scrollbar props
    <ScrollAreaPrimitive.Corner />
  </ScrollAreaPrimitive.Root>
))

//---------


const Table = React.forwardRef<
  HTMLTableElement,
  React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
  <ScrollArea className="h-60" disableScrollbar> // you can try className="h-[calc(100%)]"
    <table
      ref={ref}
      className={cn('w-full caption-bottom text-sm', className)}
      {...props}
    />
  </ScrollArea>
))

const TableHeader = React.forwardRef<
  HTMLTableSectionElement,
  React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
  <thead
    ref={ref}
    className={cn('sticky top-0 bg-secondary [&_tr]:border-b', className)}
    {...props}
  />
))

const TableBody = React.forwardRef<
  HTMLTableSectionElement,
  React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, children, ...props }, ref) => (
  <tbody
    ref={ref}
    className={cn('[&_tr:last-child]:border-0', className)}
    {...props}
  >
    {children}
    <tr> //I wrapped ScrollBar in <tr> and <td> to avoid error: "validateDOMNesting(...): <div> cannot appear as a child of <tbody>."  not sure about side effect seems functional
      <td>
        <ScrollBar className="pt-[calc(3rem+2px)]" /> // can be just className="pt-12" but I didn't like how it looked was too close to the header
      </td>
    </tr>
  </tbody>
))

https://github.com/shadcn-ui/ui/assets/54937127/95707cd8-1361-4312-8859-96fc630510c9

olzhas-kalikhan avatar Mar 14 '24 07:03 olzhas-kalikhan

Same issue, different solution to @tverderesi. I just removed the wrapping div from Table. Not sure why it is wrapped by div <div className="relative w-full overflow-auto">

how do I achieve this?

rickOsei avatar Apr 12 '24 13:04 rickOsei

UPDATE: I fixed my problem -- I just needed to remove the overflow-auto classname for <ScrollArea>, as the overflow-hidden that already exists in the base shadcn <ScrollArea> component makes it work properly (at least in my case).


I did @gdineruiz fix for my DataTable component. Works well overall, except for that breaks if I scroll too hard -- is this happening to anyone else? CleanShot 2024-04-16 at 00 14 08

My code:

table.tsx

const Table = React.forwardRef<
  HTMLTableElement,
  React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
  // <div className="relative w-full overflow-auto">
  <table
    ref={ref}
    className={cn("w-full caption-bottom text-sm", className)}
    {...props}
  />
  // </div>
));
Table.displayName = "Table";

const TableHeader = React.forwardRef<
  HTMLTableSectionElement,
  React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
  <thead
    ref={ref}
    // className={cn("[&_tr]:border-b", className)}
    // Manually added sticky top-0 to fix header not sticking to top of table
    className={cn("sticky top-0 bg-background [&_tr]:border-b", className)}
    {...props}
  />
));
TableHeader.displayName = "TableHeader";

const TableBody = React.forwardRef<
  HTMLTableSectionElement,
  React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
  <tbody
    ref={ref}
    className={cn("[&_tr:last-child]:border-0", className)}
    {...props}
  />
));
TableBody.displayName = "TableBody";

DataTable.tsx:

<div className={cn("space-y-4", `max-w-[${maxWidth}]`)}>
  <DataTableToolbar />
  <ScrollArea
    className={cn("rounded-md border", "overflow-auto", `h-[${maxHeight}px]`)}
  >
    <Table>
      <TableHeader></TableHeader>
      <TableBody></TableBody>
    </Table>
    <ScrollBar orientation="horizontal" />
  </ScrollArea>
  <DataTablePagination />
</div>;

aaron-mota avatar Apr 16 '24 05:04 aaron-mota

I have identified a solution:

  • Start by removing the div code from the table. image
  • Place a div above the table component, setting its height, and relative, and add the 'overflow-auto' class to it.
  • In the TableHeader component, apply a 'sticky' class with a 'top-0' property. image

By following these steps, you can resolve the issue of the fixed table header

Thank you!

Also if you need a border below it like me, remove border from header and add a box shadow. Otherwise border-b won't work because of border-collapse property; [&_tr]:border-0 [&_tr]:shadow-[inset_0_-1px_0] [&_tr]:shadow-border image

merthanmerter avatar Apr 17 '24 11:04 merthanmerter

Use additional claasName prop wrapperClassName, and add it to the Table div wrapper:

const Table = React.forwardRef<
  HTMLTableElement,
  React.HTMLAttributes<HTMLTableElement> & { wrapperClassName?: string }
>(({ className, wrapperClassName, ...props }, ref) => (
  <div className={cn('relative w-full overflow-auto', wrapperClassName)}>
    <table
      ref={ref}
      className={cn('w-full caption-bottom text-sm', className)}
      {...props}
    />
  </div>
))
Table.displayName = 'Table'

Then, implement sticky on the table header and add overflow-clip in wrapperClassName:

    <Table wrapperClassName="overflow-clip">
      <TableCaption>A list of your recent invoices.</TableCaption>
      <TableHeader className="sticky top-0">
        <TableRow>
          <TableHead className="w-[100px]">Invoice</TableHead>
          <TableHead>Status</TableHead>
          <TableHead>Method</TableHead>
          <TableHead className="text-right">Amount</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {invoices.map((invoice) => (
          <TableRow key={invoice.invoice}>
            <TableCell className="font-medium">{invoice.invoice}</TableCell>
            <TableCell>{invoice.paymentStatus}</TableCell>
            <TableCell>{invoice.paymentMethod}</TableCell>
            <TableCell className="text-right">{invoice.totalAmount}</TableCell>
          </TableRow>
        ))}
      </TableBody>
      <TableFooter>
        <TableRow>
          <TableCell colSpan={3}>Total</TableCell>
          <TableCell className="text-right">$2,500.00</TableCell>
        </TableRow>
      </TableFooter>
    </Table>

creotip avatar Apr 26 '24 07:04 creotip

@brijeshkumar16's answer works great.

Similarly you can also use the ScrollArea component if you want to be consistent with shadcn.

  1. Delete the div in Table
const Table = React.forwardRef<
  HTMLTableElement,
  React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
  <table
    ref={ref}
    className={cn("w-full caption-bottom text-sm", className)}
    {...props}
  />
));
Table.displayName = "Table";

Add sticky top-0 bg-secondary to TableHeader

const TableHeader = React.forwardRef<
  HTMLTableSectionElement,
  React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
  <thead
    ref={ref}
    // Manually added sticky top-0 to fix header not sticking to top of table
    className={cn("sticky top-0 bg-secondary [&_tr]:border-b", className)}
    {...props}
  />
));
TableHeader.displayName = "TableHeader";

Wrap your Table with ScrollArea and set a height

<ScrollArea className="h-[200px] rounded-md border">
 <Table>
 </Table>
</ScrollArea>

image

Works great, thanks!

kr4chinin avatar May 12 '24 19:05 kr4chinin

Fixed header overflow. Still need to set height.

const ScrollArea = React.forwardRef<
  React.ElementRef<typeof ScrollAreaPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> & {
    disableScrollbar?: boolean
  }
>(({ className, children, disableScrollbar, ...props }, ref) => (
  <ScrollAreaPrimitive.Root
    ref={ref}
    className={cn('relative overflow-hidden', className)}
    {...props}
  >
    <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
      {children}
    </ScrollAreaPrimitive.Viewport>
    {!disableScrollbar && <ScrollBar />} // need to remove default scrollbar, it overrides table scrollbar props
    <ScrollAreaPrimitive.Corner />
  </ScrollAreaPrimitive.Root>
))

//---------


const Table = React.forwardRef<
  HTMLTableElement,
  React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
  <ScrollArea className="h-60" disableScrollbar> // you can try className="h-[calc(100%)]"
    <table
      ref={ref}
      className={cn('w-full caption-bottom text-sm', className)}
      {...props}
    />
  </ScrollArea>
))

const TableHeader = React.forwardRef<
  HTMLTableSectionElement,
  React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
  <thead
    ref={ref}
    className={cn('sticky top-0 bg-secondary [&_tr]:border-b', className)}
    {...props}
  />
))

const TableBody = React.forwardRef<
  HTMLTableSectionElement,
  React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, children, ...props }, ref) => (
  <tbody
    ref={ref}
    className={cn('[&_tr:last-child]:border-0', className)}
    {...props}
  >
    {children}
    <tr> //I wrapped ScrollBar in <tr> and <td> to avoid error: "validateDOMNesting(...): <div> cannot appear as a child of <tbody>."  not sure about side effect seems functional
      <td>
        <ScrollBar className="pt-[calc(3rem+2px)]" /> // can be just className="pt-12" but I didn't like how it looked was too close to the header
      </td>
    </tr>
  </tbody>
))

Screen.Recording.2024-03-14.at.3.48.51.AM.mov

This solves the Y-axis scrolling, but the X-axis scrolling doesn't work. Does anyone have a better solution?

shuo-hiwintech avatar May 23 '24 10:05 shuo-hiwintech

Finalized X,Y axis scrolling and header fixation(I hope this helps those in the back):

image

image

shuo-hiwintech avatar May 23 '24 13:05 shuo-hiwintech

is there any way to achieve this without setting a static height ?

I've managed to not need to set a static height using css grid on the parent and max content and a max height set on the scroll area

<Table>
 <TableHeader className="sticky top-0 bg-secondary">
.....
</TableHeader>
  <TableBody className="grid auto-rows-max">
   <ScrollArea className="max-h-[200px]">
              ....
     </ScrollArea>
  </TableBody>
</Table>

This trick using css grid seems to work regularly for me with ScrollArea

React will complain that <div> cannot appear as a child of <tbody> and your table body layout will will not match headers.

faribauc avatar May 31 '24 17:05 faribauc