Repository: equinor/design-system
Pull request #3624 (2024-09-11 21:06:30)

Feat: Table footer in `eds-core-react` & `eds-data-grid-react`
## What does this pull request change?

**`feat`:**  `add Table footer component to eds-core-react`
- Implemented the Table footer component with optional sticky property for a fixed footer.
- Updated `Table.docs.mdx` to include documentation for the Table footer.
- Added Table footer to the storybook to showcase various use cases.
- Updated test suites to cover edge cases and ensure proper functionality of the Table footer.


**`feat:`** `add Table Footer component to eds-data-grid-react`
- Added the possibility to show a footer in the data grid.
- Enabled the option to make the footer fixed.
- Added `footerClass` and `footerStyle` as component props for custom styling.
- Updated docs and stories to include the new footer functionality.
- Updated test suites to cover edge cases and ensure proper functionality of the Table Footer.

**`chore`:** extract `Resizer` and `TableCell` components

- `Resizer` is now a separate module to enhance reusability and scalability.
- Renamed `Cell` to `TableCell` and moved it to a separate module for better reusability and scalability.

## Why is this pull request needed?
This pull request adds support for displaying the table footer in both **`eds-core-react`** and **`eds-data-grid-react`**. Additionally, it introduces a property that allows the footer to be fixed aka **`sticky`**.

**`Note:`** We do not have a design for the Table footer component, so it currently inherits header cell token values. This can be modified in a future update.



Patch:
diff --git a/packages/eds-core-react/src/components/Datepicker/fields/FieldWrapper.tsx b/packages/eds-core-react/src/components/Datepicker/fields/FieldWrapper.tsx
index 855dcd4b..3225d037 100644
--- a/packages/eds-core-react/src/components/Datepicker/fields/FieldWrapper.tsx
+++ b/packages/eds-core-react/src/components/Datepicker/fields/FieldWrapper.tsx
@@ -177,7 +177,6 @@ export const FieldWrapper = forwardRef(
           onClose={() => setIsOpen(false)}
           anchorEl={ref.current}
           placement={'bottom-start'}
-          withinPortal={true}
         >
           {calendar}
         </Popover>
diff --git a/packages/eds-core-react/src/components/Table/Cell.tsx b/packages/eds-core-react/src/components/Table/Cell.tsx
index f2c1a121..8164e378 100644
--- a/packages/eds-core-react/src/components/Table/Cell.tsx
+++ b/packages/eds-core-react/src/components/Table/Cell.tsx
@@ -2,6 +2,7 @@ import { TdHTMLAttributes, ThHTMLAttributes, forwardRef } from 'react'
 import { Variants, Colors } from './Table.types'
 import { TableDataCell } from './DataCell'
 import { TableHeaderCell } from './HeaderCell'
+import { TableFooterCell } from './FooterCell'
 import { InnerContext } from './Inner.context'
 
 export type CellProps = {
@@ -25,6 +26,8 @@ export const Cell = forwardRef<HTMLTableCellElement, CellProps>(function Cell(
         switch (variant) {
           case 'head':
             return <TableHeaderCell ref={ref} sticky={sticky} {...rest} />
+          case 'foot':
+            return <TableFooterCell ref={ref} sticky={sticky} {...rest} />
           default:
           case 'body':
             return <TableDataCell ref={ref} {...rest} />
diff --git a/packages/eds-core-react/src/components/Table/Foot/Foot.tokens.ts b/packages/eds-core-react/src/components/Table/Foot/Foot.tokens.ts
new file mode 100644
index 00000000..438a7fb2
--- /dev/null
+++ b/packages/eds-core-react/src/components/Table/Foot/Foot.tokens.ts
@@ -0,0 +1,26 @@
+import { tokens } from '@equinor/eds-tokens'
+import type { ComponentToken } from '@equinor/eds-tokens'
+
+const {
+  colors: {
+    ui: {
+      background__medium: { rgba: borderColor },
+    },
+    interactive: {
+      table__header__fill_resting: { rgba: backgroundColor },
+    },
+  },
+} = tokens
+
+export const token: ComponentToken = {
+  background: backgroundColor,
+  border: {
+    type: 'bordergroup',
+    bottom: {
+      type: 'border',
+      width: '2px',
+      color: borderColor,
+      style: 'solid',
+    },
+  },
+}
diff --git a/packages/eds-core-react/src/components/Table/Foot/Foot.tsx b/packages/eds-core-react/src/components/Table/Foot/Foot.tsx
new file mode 100644
index 00000000..0ca69011
--- /dev/null
+++ b/packages/eds-core-react/src/components/Table/Foot/Foot.tsx
@@ -0,0 +1,27 @@
+import { HTMLAttributes, forwardRef } from 'react'
+import styled from 'styled-components'
+import { token } from './Foot.tokens'
+import { bordersTemplate } from '@equinor/eds-utils'
+import { InnerContext } from '../Inner.context'
+
+const StyledTableFoot = styled.tfoot`
+  ${bordersTemplate(token.border)}
+  background: ${token.background};
+`
+
+export type FootProps = {
+  /** Footer will stick to bottom when scrolling */
+  sticky?: boolean
+} & HTMLAttributes<HTMLTableSectionElement>
+
+export const Foot = forwardRef<HTMLTableSectionElement, FootProps>(
+  function Foot({ children, sticky, ...props }, ref) {
+    return (
+      <InnerContext.Provider value={{ variant: 'foot', sticky }}>
+        <StyledTableFoot {...props} ref={ref}>
+          {children}
+        </StyledTableFoot>
+      </InnerContext.Provider>
+    )
+  },
+)
diff --git a/packages/eds-core-react/src/components/Table/Foot/index.ts b/packages/eds-core-react/src/components/Table/Foot/index.ts
new file mode 100644
index 00000000..ac5f14fa
--- /dev/null
+++ b/packages/eds-core-react/src/components/Table/Foot/index.ts
@@ -0,0 +1,1 @@
+export * from './Foot'
diff --git a/packages/eds-core-react/src/components/Table/FooterCell/FooterCell.tsx b/packages/eds-core-react/src/components/Table/FooterCell/FooterCell.tsx
new file mode 100644
index 00000000..f0e9fe7e
--- /dev/null
+++ b/packages/eds-core-react/src/components/Table/FooterCell/FooterCell.tsx
@@ -0,0 +1,69 @@
+import { ThHTMLAttributes, forwardRef } from 'react'
+import styled, { css, ThemeProvider } from 'styled-components'
+import {
+  typographyTemplate,
+  spacingsTemplate,
+  bordersTemplate,
+  useToken,
+} from '@equinor/eds-utils'
+import {
+  token as tableFoot,
+  TableHeadToken as TableFootToken,
+} from './../HeaderCell/HeaderCell.tokens' // Use Header cell tokens as default
+import { useEds } from '../../EdsProvider'
+
+type BaseProps = {
+  theme: TableFootToken
+  $sticky: boolean
+}
+
+const StyledTableCell = styled.th((props: BaseProps) => {
+  const { theme, $sticky } = props
+  const { background, height, typography, spacings } = theme
+
+  return css`
+    min-height: ${height};
+    height: ${height};
+    background: ${background};
+    box-sizing: border-box;
+    ${spacingsTemplate(spacings)}
+    ${typographyTemplate(typography)}
+  ${bordersTemplate(theme.border)}
+  ${$sticky
+      ? css`
+          position: sticky;
+          bottom: 0;
+          z-index: 2;
+        `
+      : ''}
+  `
+})
+
+const CellInner = styled.div`
+  display: flex;
+  align-items: center;
+`
+
+type CellProps = {
+  sticky?: boolean
+} & ThHTMLAttributes<HTMLTableCellElement>
+
+export const TableFooterCell = forwardRef<HTMLTableCellElement, CellProps>(
+  function TableFooterCell({ children, sticky, ...rest }, ref) {
+    const { density } = useEds()
+    const token = useToken({ density }, tableFoot)
+    const props = {
+      ref,
+      $sticky: sticky,
+      ...rest,
+    }
+
+    return (
+      <ThemeProvider theme={token}>
+        <StyledTableCell {...props}>
+          <CellInner>{children}</CellInner>
+        </StyledTableCell>
+      </ThemeProvider>
+    )
+  },
+)
diff --git a/packages/eds-core-react/src/components/Table/FooterCell/index.ts b/packages/eds-core-react/src/components/Table/FooterCell/index.ts
new file mode 100644
index 00000000..ed29c1d2
--- /dev/null
+++ b/packages/eds-core-react/src/components/Table/FooterCell/index.ts
@@ -0,0 +1,1 @@
+export * from './FooterCell'
diff --git a/packages/eds-core-react/src/components/Table/Inner.context.tsx b/packages/eds-core-react/src/components/Table/Inner.context.tsx
index 61519db0..4776b573 100644
--- a/packages/eds-core-react/src/components/Table/Inner.context.tsx
+++ b/packages/eds-core-react/src/components/Table/Inner.context.tsx
@@ -1,12 +1,12 @@
 import { createContext } from 'react'
 
 type State = {
-  variant: 'body' | 'head'
+  variant: 'body' | 'head' | 'foot'
   sticky?: boolean
 }
 
-const initalState: State = {
+const initialState: State = {
   variant: 'body',
 }
 
-export const InnerContext = createContext<State>(initalState)
+export const InnerContext = createContext<State>(initialState)
diff --git a/packages/eds-core-react/src/components/Table/Table.docs.mdx b/packages/eds-core-react/src/components/Table/Table.docs.mdx
index 484c28a6..d0be31ad 100644
--- a/packages/eds-core-react/src/components/Table/Table.docs.mdx
+++ b/packages/eds-core-react/src/components/Table/Table.docs.mdx
@@ -42,6 +42,15 @@ Tables display information in an easy to scan format.
     <li>Icons can be used for sorting columns as well as a alternative text.</li>
 </ul>
 
+
+**Table Footer**
+<ul>
+    <li>The table footer is used to display summary information, totals, or additional notes that relate to the data presented in the table body.</li>
+    <li> It serves as the concluding section of the table, often providing context or aggregated data that complements the table's content.</li>
+    <li>The width of footer cells is customizable using the `colSpan` prop, allowing footer cells to span multiple columns. This is useful for displaying summary data or totals that need to align visually across the table.</li>
+</ul>
+
+
 **Table Cell**
 <ul>
     <li>The cell row can have icons, text, links, inputs, numbers, monospaced numbers and other custom content (by using the placeholder).</li>
@@ -80,6 +89,11 @@ const Demo = () => {
                 <Table.Cell>2.5</Table.Cell>
             </Table.Row>
         </Table.Body>
+        <Table.Foot>
+           <Table.Row>
+                <Table.Cell colspan={3}>Footer</Table.Cell>
+            </Table.Row>
+        </Table.Foot>
     </Table>
     )
 }
@@ -87,6 +101,12 @@ const Demo = () => {
 
 ## Examples
 
+### Fixed Table Header & Footer
+
+<Canvas of={ComponentStories.FixedTableHeaderAndFooter} />
+
+
+
 ### Fixed table header
 
 <Canvas of={ComponentStories.FixedTableHeader} />
@@ -108,3 +128,7 @@ To render very large datasets, "virtual scrolling" might sometimes be a good opt
 combination with 3rd party solutions such as <a href="https://tanstack.com/virtual/v3">react-virtual</a> to achieve this.
 
 <Canvas of={ComponentStories.VirtualScrolling} />
+
+
+### Virtual scrolling with fixed footer
+<Canvas of={ComponentStories.VirtualScrollingWithFixedFooter} />
diff --git a/packages/eds-core-react/src/components/Table/Table.stories.tsx b/packages/eds-core-react/src/components/Table/Table.stories.tsx
index aca758d9..bb66681e 100644
--- a/packages/eds-core-react/src/components/Table/Table.stories.tsx
+++ b/packages/eds-core-react/src/components/Table/Table.stories.tsx
@@ -22,12 +22,12 @@ import page from './Table.docs.mdx'
 
 Icon.add({ arrow_down, arrow_up })
 
-const { Caption, Body, Row, Cell, Head } = Table
+const { Caption, Body, Row, Cell, Head, Foot } = Table
 
 const meta: Meta<typeof Table> = {
   title: 'Data Display/Table',
   component: Table,
-  subcomponents: { Caption, Head, Body, Cell, Row },
+  subcomponents: { Caption, Head, Body, Cell, Row, Foot },
   parameters: {
     docs: {
       page,
@@ -66,6 +66,11 @@ export const introduction: StoryFn<TableProps> = (args) => {
           </Table.Row>
         ))}
       </Table.Body>
+      <Table.Foot>
+        <Table.Row>
+          <Table.Cell colSpan={columns.length}>Footer</Table.Cell>
+        </Table.Row>
+      </Table.Foot>
     </Table>
   )
 }
@@ -113,6 +118,46 @@ export const FixedTableHeader: StoryFn<TableProps> = () => {
 }
 FixedTableHeader.storyName = 'Fixed table header'
 
+export const FixedTableHeaderAndFooter: StoryFn<TableProps> = () => {
+  const cellValues = toCellValues(data, columns)
+  const total = data.reduce((acc, curr) => acc + curr?.price, 0)
+  return (
+    <div style={{ height: '400px', overflow: 'auto', display: 'grid' }}>
+      <Table>
+        <Table.Caption>
+          <Typography variant="h2" style={{ marginBottom: '16px' }}>
+            Fruits cost price
+          </Typography>
+        </Table.Caption>
+        <Table.Head sticky>
+          <Table.Row>
+            {columns.map((col) => (
+              <Table.Cell key={`head-${col.accessor}`}>{col.name}</Table.Cell>
+            ))}
+          </Table.Row>
+        </Table.Head>
+        <Table.Body>
+          {cellValues?.map((row) => (
+            <Table.Row key={row.toString()}>
+              {row.map((cellValue) => (
+                <Table.Cell key={cellValue}>{cellValue}</Table.Cell>
+              ))}
+            </Table.Row>
+          ))}
+        </Table.Body>
+        <Table.Foot sticky>
+          <Table.Row>
+            <Table.Cell colSpan={columns?.length - 1}>Total</Table.Cell>
+            <Table.Cell colSpan={1}>{total}</Table.Cell>
+          </Table.Row>
+        </Table.Foot>
+      </Table>
+    </div>
+  )
+}
+
+FixedTableHeaderAndFooter.storyName = 'Fixed table header and footer'
+
 export const CompactTable: StoryFn<TableProps> = () => {
   const cellValues = toCellValues(data, columns)
 
@@ -213,6 +258,11 @@ export const CompactTable: StoryFn<TableProps> = () => {
               </Table.Row>
             ))}
           </Table.Body>
+          <Table.Foot>
+            <Table.Row>
+              <Table.Cell colSpan={5}>Footer</Table.Cell>
+            </Table.Row>
+          </Table.Foot>
         </Table>
       </EdsProvider>
     </>
@@ -343,6 +393,11 @@ export const Sortable: StoryFn<TableProps> = () => {
           </Table.Row>
         ))}
       </Table.Body>
+      <Table.Foot>
+        <Table.Row>
+          <Table.Cell colSpan={5}>Footer</Table.Cell>
+        </Table.Row>
+      </Table.Foot>
     </Table>
   )
 }
@@ -364,6 +419,133 @@ type Photo = {
   thumbnailUrl: string
 }
 
+export const VirtualScrollingWithFixedFooter: StoryFn<TableProps> = () => {
+  const [data, setData] = useState<Array<Photo>>([])
+  const parentRef = useRef()
+
+  const estimateSize = useCallback(() => {
+    return 48
+  }, [])
+
+  const virtualizer = useVirtualizer({
+    count: data.length,
+    getScrollElement: () => parentRef.current,
+    estimateSize,
+  })
+
+  useEffect(() => {
+    const abortController = new AbortController()
+    const signal = abortController.signal
+
+    fetch(`https://jsonplaceholder.typicode.com/photos`, { signal })
+      .then((r) => r.json())
+      .then((d: Photo[]) => {
+        setData(d.slice(0, 100))
+      })
+      .catch((err: Error) => {
+        console.error(`Error: ${err.message}`)
+      })
+    return () => {
+      abortController.abort()
+    }
+  }, [])
+
+  const virtualRows = virtualizer.getVirtualItems()
+  const paddingTop = virtualRows.length ? virtualRows[0].start : 0
+  const paddingBottom = virtualRows.length
+    ? virtualizer.getTotalSize() - virtualRows[virtualRows.length - 1].end
+    : 0
+
+  return (
+    <div
+      style={{
+        height: '600px',
+        overflow: 'auto',
+      }}
+      ref={parentRef}
+    >
+      <Table
+        style={{ width: '100%', paddingLeft: '15px', paddingRight: '15px' }}
+      >
+        <Table.Head sticky>
+          <Table.Row>
+            <Table.Cell>
+              <div style={{ width: '40px' }}>ID</div>
+            </Table.Cell>
+            <Table.Cell>
+              <div style={{ width: '70px' }}>Album ID</div>
+            </Table.Cell>
+            <Table.Cell>
+              <div style={{ width: '400px' }}>Title</div>
+            </Table.Cell>
+            <Table.Cell>
+              <div style={{ width: '120px' }}>URL</div>
+            </Table.Cell>
+            <Table.Cell>
+              <div style={{ width: '120px' }}>Thumbnail url</div>
+            </Table.Cell>
+          </Table.Row>
+        </Table.Head>
+        <Table.Body>
+          <Table.Row>
+            <Table.Cell style={{ height: `${paddingTop}px` }}></Table.Cell>
+          </Table.Row>
+          {virtualRows.map((virtualRow) => {
+            const row: Photo = data[virtualRow.index]
+
+            return (
+              <Table.Row key={row.id}>
+                <Table.Cell>
+                  <div style={{ width: '40px' }}>{row.id}</div>
+                </Table.Cell>
+                <Table.Cell>
+                  <div style={{ width: '70px' }}>{row.albumId}</div>
+                </Table.Cell>
+                <Table.Cell>
+                  <div
+                    style={{
+                      width: '400px',
+                      whiteSpace: 'nowrap',
+                      overflow: 'hidden',
+                      textOverflow: 'ellipsis',
+                    }}
+                  >
+                    {row.title}
+                  </div>
+                </Table.Cell>
+                <Table.Cell>
+                  <div style={{ width: '120px' }}>
+                    {' '}
+                    <Typography link href={row.url} target="_blank">
+                      Open image
+                    </Typography>
+                  </div>
+                </Table.Cell>
+                <Table.Cell>
+                  <div style={{ width: '120px' }}>
+                    {' '}
+                    <Typography link href={row.thumbnailUrl} target="_blank">
+                      Open thumbnail
+                    </Typography>
+                  </div>
+                </Table.Cell>
+              </Table.Row>
+            )
+          })}
+          <Table.Row>
+            <Table.Cell style={{ height: `${paddingBottom}px` }}></Table.Cell>
+          </Table.Row>
+        </Table.Body>
+        <Table.Foot sticky>
+          <Table.Row>
+            <Table.Cell colSpan={5}>{`Total ${data?.length} items`}</Table.Cell>
+          </Table.Row>
+        </Table.Foot>
+      </Table>
+    </div>
+  )
+}
+
 export const VirtualScrolling: StoryFn<TableProps> = () => {
   const [data, setData] = useState<Array<Photo>>([])
   const parentRef = useRef()
@@ -481,6 +663,11 @@ export const VirtualScrolling: StoryFn<TableProps> = () => {
             <Table.Cell style={{ height: `${paddingBottom}px` }}></Table.Cell>
           </Table.Row>
         </Table.Body>
+        <Table.Foot>
+          <Table.Row>
+            <Table.Cell colSpan={5}>Footer</Table.Cell>
+          </Table.Row>
+        </Table.Foot>
       </Table>
     </div>
   )
diff --git a/packages/eds-core-react/src/components/Table/index.tsx b/packages/eds-core-react/src/components/Table/index.tsx
index b70971a2..03418cde 100644
--- a/packages/eds-core-react/src/components/Table/index.tsx
+++ b/packages/eds-core-react/src/components/Table/index.tsx
@@ -2,6 +2,7 @@ import { Table as BaseTable, TableProps } from './Table'
 import { Body, BodyProps } from './Body'
 import { Cell, CellProps } from './Cell'
 import { Head, HeadProps } from './Head'
+import { Foot, FootProps } from './Foot'
 import { Row, RowProps } from './Row'
 import { Caption, CaptionProps } from './Caption'
 
@@ -9,6 +10,7 @@ type TableCompoundProps = typeof BaseTable & {
   Body: typeof Body
   Cell: typeof Cell
   Head: typeof Head
+  Foot: typeof Foot
   Row: typeof Row
   Caption: typeof Caption
 }
@@ -18,12 +20,14 @@ const Table = BaseTable as TableCompoundProps
 Table.Body = Body
 Table.Cell = Cell
 Table.Head = Head
+Table.Foot = Foot
 Table.Row = Row
 Table.Caption = Caption
 
 Table.Body.displayName = 'Table.Body'
 Table.Cell.displayName = 'Table.Cell'
 Table.Head.displayName = 'Table.Head'
+Table.Foot.displayName = 'Table.Foot'
 Table.Row.displayName = 'Table.Row'
 Table.Caption.displayName = 'Table.Caption'
 
@@ -35,4 +39,5 @@ export type {
   RowProps,
   CaptionProps,
   HeadProps,
+  FootProps,
 }
diff --git a/packages/eds-data-grid-react/src/EdsDataGrid.docs.mdx b/packages/eds-data-grid-react/src/EdsDataGrid.docs.mdx
index 10c6c5c2..8b4d3734 100644
--- a/packages/eds-data-grid-react/src/EdsDataGrid.docs.mdx
+++ b/packages/eds-data-grid-react/src/EdsDataGrid.docs.mdx
@@ -173,5 +173,7 @@ Comes with 4 different ways of styling built in:
 - cellStyle: Allows to add custom css to a cell (td) based on a function
 - headerClass: Allows to add a class to a header cell (th) based on a function
 - headerStyle: Allows to add custom css to a header cell (th) based on a function
+- footerClass: Allows to add a class to a footer cell (th) based on a function
+- footerStyle: Allows to add custom css to a footer cell (th) based on a function
 
 <Canvas of={ComponentStories.Styling} />
diff --git a/packages/eds-data-grid-react/src/EdsDataGrid.stories.tsx b/packages/eds-data-grid-react/src/EdsDataGrid.stories.tsx
index b87dd7ac..4d55213c 100644
--- a/packages/eds-data-grid-react/src/EdsDataGrid.stories.tsx
+++ b/packages/eds-data-grid-react/src/EdsDataGrid.stories.tsx
@@ -26,12 +26,14 @@ import { FilterWrapper } from './components/FilterWrapper'
 import {
   Photo,
   PostComment,
+  Summary,
+  aggregatedSummaryColumns,
   columns,
   expandColumns,
   groupedColumns,
   helper,
 } from './stories/columns'
-import { data } from './stories/data'
+import { data, summaryData } from './stories/data'
 import { Virtualizer } from './types'
 
 const meta: Meta<typeof EdsDataGrid<Photo>> = {
@@ -392,6 +394,38 @@ ColumnPinning.args = {
   columns: columns,
   height: 500,
   rows: data,
+  enableFooter: true,
+  stickyFooter: true,
+}
+
+export const ColumnPinningWithFooter: StoryFn<EdsDataGridProps<Summary>> = (
+  args,
+) => {
+  const { columnPinState } = args
+  return (
+    <>
+      <Typography as={'div'} style={{ whiteSpace: 'pre' }}>
+        {JSON.stringify(columnPinState, null, 2)}
+      </Typography>
+      <EdsDataGrid {...args} />
+    </>
+  )
+}
+
+ColumnPinningWithFooter.args = {
+  columnPinState: {
+    left: [aggregatedSummaryColumns.at(0).id],
+  },
+  scrollbarHorizontal: true,
+  stickyHeader: true,
+  width: 700,
+  columns: aggregatedSummaryColumns,
+  height: 500,
+  rows: summaryData,
+  enableFooter: true,
+  stickyFooter: true,
+  headerClass: () => 'header-class',
+  footerClass: () => 'footer-class',
 }
 
 export const ColumnOrdering: StoryFn<EdsDataGridProps<Photo>> = (args) => {
diff --git a/packages/eds-data-grid-react/src/EdsDataGrid.tsx b/packages/eds-data-grid-react/src/EdsDataGrid.tsx
index 52e1b2c1..9c1d89ac 100644
--- a/packages/eds-data-grid-react/src/EdsDataGrid.tsx
+++ b/packages/eds-data-grid-react/src/EdsDataGrid.tsx
@@ -32,6 +32,7 @@ import styled from 'styled-components'
 import { TableProvider } from './EdsDataGridContext'
 import { EdsDataGridProps } from './EdsDataGridProps'
 import { TableHeaderRow } from './components/TableHeaderRow'
+import { TableFooterRow } from './components/TableFooterRow'
 import { TableRow } from './components/TableRow'
 import {
   addPxSuffixIfInputHasNoPrefix,
@@ -54,6 +55,7 @@ export function EdsDataGrid<T>({
   enablePagination,
   enableSorting,
   stickyHeader,
+  stickyFooter,
   onSelectRow,
   onRowSelectionChange,
   caption,
@@ -69,6 +71,8 @@ export function EdsDataGrid<T>({
   rowStyle,
   headerClass,
   headerStyle,
+  footerClass,
+  footerStyle,
   externalPaginator,
   onSortingChange,
   manualSorting,
@@ -88,6 +92,7 @@ export function EdsDataGrid<T>({
   defaultColumn,
   onRowClick,
   onCellClick,
+  enableFooter,
 }: EdsDataGridProps<T>) {
   logDevelopmentWarningOfPropUse({
     virtualHeight: {
@@ -365,6 +370,7 @@ export function EdsDataGrid<T>({
   // These classes are primarily used to allow for feature-detection in the test-suite
   const classList = {
     'sticky-header': !!stickyHeader,
+    'sticky-footer': !!stickyFooter,
     virtual: !!enableVirtual,
     paging: !!enablePagination,
   }
@@ -377,10 +383,13 @@ export function EdsDataGrid<T>({
       rowStyle={rowStyle}
       headerClass={headerClass}
       headerStyle={headerStyle}
+      footerClass={footerClass}
+      footerStyle={footerStyle}
       table={table}
       enableSorting={!!enableSorting}
       enableColumnFiltering={!!enableColumnFiltering}
       stickyHeader={!!stickyHeader}
+      stickyFooter={!!stickyFooter}
     >
       <TableWrapper
         className="table-wrapper"
@@ -441,7 +450,6 @@ export function EdsDataGrid<T>({
 
                 {virtualRows.map((virtualItem) => {
                   const row = table.getRowModel().rows[virtualItem.index]
-
                   return (
                     <TableRow
                       key={virtualItem.index}
@@ -485,6 +493,19 @@ export function EdsDataGrid<T>({
                   />
                 ))}
           </Table.Body>
+          {enableFooter && (
+            <Table.Foot sticky={stickyFooter} data-testid="eds-grid-footer">
+              {table.getFooterGroups().map((footerGroup) => (
+                <TableFooterRow
+                  key={footerGroup.id}
+                  table={table}
+                  footerGroup={footerGroup}
+                  columnResizeMode={columnResizeMode}
+                  deltaOffset={table.getState().columnSizingInfo.deltaOffset}
+                />
+              ))}
+            </Table.Foot>
+          )}
         </Table>
         {externalPaginator
           ? externalPaginator
diff --git a/packages/eds-data-grid-react/src/EdsDataGridContext.tsx b/packages/eds-data-grid-react/src/EdsDataGridContext.tsx
index 303f828c..b42e0fb7 100644
--- a/packages/eds-data-grid-react/src/EdsDataGridContext.tsx
+++ b/packages/eds-data-grid-react/src/EdsDataGridContext.tsx
@@ -4,6 +4,7 @@ import { Column, Row, Table as TanStackTable } from '@tanstack/react-table'
 type Context<T> = {
   enableSorting: boolean
   stickyHeader: boolean
+  stickyFooter: boolean
   enableColumnFiltering: boolean
   table: TanStackTable<T> | null
   cellClass?: (row: Row<T>, columnId: string) => string
@@ -12,12 +13,15 @@ type Context<T> = {
   rowStyle?: (row: Row<T>) => CSSProperties
   headerClass?: (column: Column<T>) => string
   headerStyle?: (column: Column<T>) => CSSProperties
+  footerClass?: (column: Column<T>) => string
+  footerStyle?: (column: Column<T>) => CSSProperties
 }
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
 export const EdsDataGridContext = createContext<Context<any>>({
   enableSorting: false,
   stickyHeader: false,
+  stickyFooter: false,
   enableColumnFiltering: false,
   table: null,
 })
diff --git a/packages/eds-data-grid-react/src/EdsDataGridProps.ts b/packages/eds-data-grid-react/src/EdsDataGridProps.ts
index ca320d30..460453aa 100644
--- a/packages/eds-data-grid-react/src/EdsDataGridProps.ts
+++ b/packages/eds-data-grid-react/src/EdsDataGridProps.ts
@@ -46,6 +46,11 @@ type BaseProps<T> = {
    * @default false
    */
   stickyHeader?: boolean
+  /**
+   * Make the table footer sticky
+   * @default false
+   */
+  stickyFooter?: boolean
   /**
    * Element to display in Table.Caption
    * @default undefined
@@ -95,6 +100,10 @@ type BaseProps<T> = {
    * @link [API Docs](https://tanstack.com/table/v8/docs/api/core/table#defaultcolumn)
    */
   defaultColumn?: Partial<ColumnDef<T, unknown>>
+  /**
+   * Optional props to show table footer
+   */
+  enableFooter?: boolean
 }
 
 type RowSelectionProps<T> = {
@@ -179,6 +188,16 @@ type StyleProps<T> = {
    * @param column
    */
   headerStyle?: (column: Column<T>) => CSSProperties
+  /**
+   * Function that can be used to set custom classes on a footer cell
+   * @param column
+   */
+  footerClass?: (column: Column<T>) => string
+  /**
+   * Function that can be used to set custom styles on a footer cell
+   * @param column
+   */
+  footerStyle?: (column: Column<T>) => CSSProperties
 }
 
 type FilterProps = {
diff --git a/packages/eds-data-grid-react/src/components/Resizer.tsx b/packages/eds-data-grid-react/src/components/Resizer.tsx
new file mode 100644
index 00000000..706b866e
--- /dev/null
+++ b/packages/eds-data-grid-react/src/components/Resizer.tsx
@@ -0,0 +1,33 @@
+import { ColumnResizeMode } from '@tanstack/react-table'
+import styled from 'styled-components'
+
+type ResizeProps = {
+  $columnResizeMode: ColumnResizeMode | null | undefined
+  $isResizing: boolean
+}
+
+export const ResizeInner = styled.div`
+  width: 2px;
+  opacity: 0;
+  height: 100%;
+`
+
+export const Resizer = styled.div<ResizeProps>`
+  transform: ${(props) =>
+    props.$columnResizeMode === 'onEnd' ? 'translateX(0px)' : 'none'};
+
+  ${ResizeInner} {
+    opacity: ${(props) => (props.$isResizing ? 1 : 0)};
+  }
+
+  position: absolute;
+  right: 0;
+  top: 0;
+  height: 100%;
+  width: 5px;
+  cursor: col-resize;
+  user-select: none;
+  touch-action: none;
+  display: flex;
+  justify-content: flex-end;
+`
diff --git a/packages/eds-data-grid-react/src/components/TableCell.tsx b/packages/eds-data-grid-react/src/components/TableCell.tsx
new file mode 100644
index 00000000..984af3b7
--- /dev/null
+++ b/packages/eds-data-grid-react/src/components/TableCell.tsx
@@ -0,0 +1,29 @@
+import { Table } from '@equinor/eds-core-react'
+import { tokens } from '@equinor/eds-tokens'
+import { ColumnPinningPosition } from '@tanstack/react-table'
+import styled from 'styled-components'
+import { ResizeInner } from './Resizer'
+
+export const TableCell = styled(Table.Cell)<{
+  $sticky: boolean
+  $pinned: ColumnPinningPosition
+  $offset: number
+}>`
+  font-weight: bold;
+  position: ${(p) => (p.$sticky || p.$pinned ? 'sticky' : 'relative')};
+  top: 0;
+  ${(p) => {
+    if (p.$pinned) {
+      return `${p.$pinned}: ${p.$offset}px;`
+    }
+    return ''
+  }}
+  ${(p) => {
+    if (p.$sticky && p.$pinned) return 'z-index: 13'
+    if (p.$sticky || p.$pinned) return 'z-index: 12'
+  }};
+  &:hover ${ResizeInner} {
+    background: ${tokens.colors.interactive.primary__hover.rgba};
+    opacity: 1;
+  }
+`
diff --git a/packages/eds-data-grid-react/src/components/TableFooterCell.tsx b/packages/eds-data-grid-react/src/components/TableFooterCell.tsx
new file mode 100644
index 00000000..a2418b4d
--- /dev/null
+++ b/packages/eds-data-grid-react/src/components/TableFooterCell.tsx
@@ -0,0 +1,85 @@
+import {
+  ColumnResizeMode,
+  flexRender,
+  Header,
+  Table as TanStackTable,
+} from '@tanstack/react-table'
+import { useTableContext } from '../EdsDataGridContext'
+import { useMemo } from 'react'
+import { SortIndicator } from './SortIndicator'
+import { ResizeInner, Resizer } from './Resizer'
+import { TableCell } from './TableCell'
+
+type Props<T> = {
+  footer: Header<T, unknown>
+  columnResizeMode: ColumnResizeMode | null | undefined
+  // eslint-disable-next-line react/no-unused-prop-types
+  deltaOffset: number | null
+  // eslint-disable-next-line react/no-unused-prop-types
+  table: TanStackTable<T>
+}
+
+export function TableFooterCell<T>({ footer, columnResizeMode }: Props<T>) {
+  const ctx = useTableContext()
+  const table = ctx.table
+  const pinned = footer.column.getIsPinned()
+  const offset = useMemo<number>(() => {
+    if (!pinned) {
+      return null
+    }
+    return pinned === 'left'
+      ? footer.getStart()
+      : table.getTotalSize() - footer.getStart() - footer.getSize()
+  }, [pinned, footer, table])
+  return footer.isPlaceholder ? (
+    <TableCell
+      $sticky={ctx.stickyFooter}
+      $offset={offset}
+      $pinned={pinned}
+      className={ctx.footerClass ? ctx.footerClass(footer.column) : ''}
+      style={{
+        ...(ctx.footerStyle ? ctx.footerStyle(footer.column) : {}),
+      }}
+      aria-hidden={true}
+    />
+  ) : (
+    <TableCell
+      $sticky={ctx.stickyFooter}
+      $offset={offset}
+      $pinned={pinned}
+      className={ctx.footerClass ? ctx.footerClass(footer.column) : ''}
+      key={footer.id}
+      onClick={footer.column.getToggleSortingHandler()}
+      colSpan={footer.colSpan}
+      style={{
+        width: footer.getSize(),
+        verticalAlign: ctx.enableColumnFiltering ? 'top' : 'middle',
+        ...(ctx.footerStyle ? ctx.footerStyle(footer.column) : {}),
+      }}
+    >
+      <>
+        <div style={{ display: 'flex', flexDirection: 'column' }}>
+          <span className="table-footer-cell-label">
+            {flexRender(footer.column.columnDef.footer, footer.getContext())}
+          </span>
+        </div>
+        {!footer.column.columnDef.meta?.customFilterInput && (
+          <SortIndicator column={footer.column} />
+        )}
+      </>
+      {columnResizeMode && (
+        <Resizer
+          onClick={(e) => e.stopPropagation()}
+          onMouseDown={footer.getResizeHandler()}
+          onTouchStart={footer.getResizeHandler()}
+          $isResizing={footer.column.getIsResizing()}
+          $columnResizeMode={columnResizeMode}
+          className={'resize-handle'}
+          data-testid={'resize-handle'}
+        >
+          <ResizeInner />
+        </Resizer>
+      )}
+    </TableCell>
+  )
+}
diff --git a/packages/eds-data-grid-react/src/components/TableFooterRow.tsx b/packages/eds-data-grid-react/src/components/TableFooterRow.tsx
new file mode 100644
index 00000000..9ffdfabd
--- /dev/null
+++ b/packages/eds-data-grid-react/src/components/TableFooterRow.tsx
@@ -0,0 +1,35 @@
+import { Table } from '@equinor/eds-core-react'
+import {
+  ColumnResizeMode,
+  HeaderGroup,
+  Table as TanStackTable,
+} from '@tanstack/react-table'
+import { TableFooterCell } from './TableFooterCell'
+
+type Props<T> = {
+  footerGroup: HeaderGroup<T>
+  columnResizeMode?: ColumnResizeMode | null
+  deltaOffset: number | null
+  table: TanStackTable<T>
+}
+
+export function TableFooterRow<T>({
+  footerGroup,
+  columnResizeMode,
+  deltaOffset,
+  table,
+}: Props<T>) {
+  return (
+    <Table.Row>
+      {footerGroup.headers.map((footer) => (
+        <TableFooterCell
+          footer={footer}
+          key={footer.id}
+          table={table}
+          columnResizeMode={columnResizeMode}
+          deltaOffset={deltaOffset}
+        />
+      ))}
+    </Table.Row>
+  )
+}
diff --git a/packages/eds-data-grid-react/src/components/TableHeaderCell.tsx b/packages/eds-data-grid-react/src/components/TableHeaderCell.tsx
index e264fd34..aa3b1a4e 100644
--- a/packages/eds-data-grid-react/src/components/TableHeaderCell.tsx
+++ b/packages/eds-data-grid-react/src/components/TableHeaderCell.tsx
@@ -1,18 +1,16 @@
 import {
-  ColumnPinningPosition,
   ColumnResizeMode,
   flexRender,
   Header,
   SortDirection,
   Table as TanStackTable,
 } from '@tanstack/react-table'
-import { Table } from '@equinor/eds-core-react'
 import { useTableContext } from '../EdsDataGridContext'
-import styled from 'styled-components'
-import { tokens } from '@equinor/eds-tokens'
 import { useMemo } from 'react'
 import { FilterWrapper } from './FilterWrapper'
 import { SortIndicator } from './SortIndicator'
+import { ResizeInner, Resizer } from './Resizer'
+import { TableCell } from './TableCell'
 
 type Props<T> = {
   header: Header<T, unknown>
@@ -32,61 +30,6 @@ const getSortLabel = (
   return 'none'
 }
 
-type ResizeProps = {
-  $columnResizeMode: ColumnResizeMode | null | undefined
-  $isResizing: boolean
-}
-
-const ResizeInner = styled.div`
-  width: 2px;
-  opacity: 0;
-  height: 100%;
-`
-
-const Resizer = styled.div<ResizeProps>`
-  transform: ${(props) =>
-    props.$columnResizeMode === 'onEnd' ? 'translateX(0px)' : 'none'};
-
-  ${ResizeInner} {
-    opacity: ${(props) => (props.$isResizing ? 1 : 0)};
-  }
-
-  position: absolute;
-  right: 0;
-  top: 0;
-  height: 100%;
-  width: 5px;
-  cursor: col-resize;
-  user-select: none;
-  touch-action: none;
-  display: flex;
-  justify-content: flex-end;
-`
-
-const Cell = styled(Table.Cell)<{
-  $sticky: boolean
-  $pinned: ColumnPinningPosition
-  $offset: number
-}>`
-  font-weight: bold;
-  position: ${(p) => (p.$sticky || p.$pinned ? 'sticky' : 'relative')};
-  top: 0;
-  ${(p) => {
-    if (p.$pinned) {
-      return `${p.$pinned}: ${p.$offset}px;`
-    }
-    return ''
-  }}
-  ${(p) => {
-    if (p.$sticky && p.$pinned) return 'z-index: 13'
-    if (p.$sticky || p.$pinned) return 'z-index: 12'
-  }};
-  &:hover ${ResizeInner} {
-    background: ${tokens.colors.interactive.primary__hover.rgba};
-    opacity: 1;
-  }
-`
-
 export function TableHeaderCell<T>({ header, columnResizeMode }: Props<T>) {
   const ctx = useTableContext()
   const table = ctx.table
@@ -100,7 +43,7 @@ export function TableHeaderCell<T>({ header, columnResizeMode }: Props<T>) {
       : table.getTotalSize() - header.getStart() - header.getSize()
   }, [pinned, header, table])
   return header.isPlaceholder ? (
-    <Cell
+    <TableCell
       $sticky={ctx.stickyHeader}
       $offset={offset}
       $pinned={pinned}
@@ -111,7 +54,7 @@ export function TableHeaderCell<T>({ header, columnResizeMode }: Props<T>) {
       aria-hidden={true}
     />
   ) : (
-    <Cell
+    <TableCell
       $sticky={ctx.stickyHeader}
       $offset={offset}
       $pinned={pinned}
@@ -158,6 +101,6 @@ export function TableHeaderCell<T>({ header, columnResizeMode }: Props<T>) {
           <ResizeInner />
         </Resizer>
       )}
-    </Cell>
+    </TableCell>
   )
 }
diff --git a/packages/eds-data-grid-react/src/stories/columns.tsx b/packages/eds-data-grid-react/src/stories/columns.tsx
index f7ad70fc..2059acdd 100644
--- a/packages/eds-data-grid-react/src/stories/columns.tsx
+++ b/packages/eds-data-grid-react/src/stories/columns.tsx
@@ -13,12 +13,64 @@ export type Photo = {
   timestamp?: Date
 }
 
-export const helper = createColumnHelper<Photo>()
+export type Summary = {
+  id: string
+  country: string
+  price: number
+  tax: number
+  discount: number
+}
 
 const Link = styled.a`
   color: ${tokens.colors.interactive.primary__resting.rgba};
 `
 
+export const summaryHelper = createColumnHelper<Summary>()
+export const aggregatedSummaryColumns: Array<ColumnDef<Summary>> = [
+  summaryHelper.accessor('country', {
+    header: 'Country',
+    footer: () => 'Total',
+    size: 150,
+    id: 'country',
+  }),
+  summaryHelper.accessor('price', {
+    header: 'Price',
+    footer: (props) => {
+      const price = props.table
+        .getCoreRowModel()
+        .rows.reduce((acc, curr) => (acc = acc + curr?.original?.price || 0), 0)
+      return <span>{price.toFixed(2)}</span>
+    },
+  }),
+  summaryHelper.accessor('tax', {
+    header: 'Tax',
+    footer: (props) => {
+      const price = props.table
+        .getCoreRowModel()
+        .rows.reduce((acc, curr) => (acc = acc + curr?.original?.tax || 0), 0)
+      return <span>{price.toFixed(2)}</span>
+    },
+    size: 250,
+    id: 'tax',
+  }),
+  summaryHelper.accessor('discount', {
+    header: 'Discount',
+    footer: (props) => {
+      const price = props.table
+        .getCoreRowModel()
+        .rows.reduce(
+          (acc, curr) => (acc = acc + curr?.original?.discount || 0),
+          0,
+        )
+      return <span>{price.toFixed(2)}</span>
+    },
+    size: 250,
+    id: 'discount',
+  }),
+]
+
+export const helper = createColumnHelper<Photo>()
+
 export const columns: Array<ColumnDef<Photo>> = [
   helper.accessor('id', {
     header: () => <span style={{ fontStyle: 'italic' }}>ID</span>,
diff --git a/packages/eds-data-grid-react/src/stories/data.ts b/packages/eds-data-grid-react/src/stories/data.ts
index df517044..7e3e74c0 100644
--- a/packages/eds-data-grid-react/src/stories/data.ts
+++ b/packages/eds-data-grid-react/src/stories/data.ts
@@ -1,6 +1,6 @@
 // Subset of data from https://jsonplaceholder.typicode.com/photos
 
-import { Photo } from './columns'
+import { Photo, Summary } from './columns'
 
 const getRandomDate = () => {
   const date = new Date()
@@ -239,3 +239,489 @@ export const data: Array<Photo> = [
     url: 'https://via.placeholder.com/600/372c93',
   },
 ]
+
+export const summaryData: Array<Summary> = [
+  {
+    id: 'f29f0487-2266-40d1-947e-0a0926ad2631',
+    country: 'UK',
+    price: 101.7,
+    tax: 9.78,
+    discount: 28.37,
+  },
+  {
+    id: 'e51d8bca-afac-48ee-8eb9-72b754abf4a1',
+    country: 'Japan',
+    price: 753.76,
+    tax: 1.62,
+    discount: 9.67,
+  },
+  {
+    id: 'ea59a527-e809-416c-acb4-2d576c8239aa',
+    country: 'Germany',
+    price: 230.66,
+    tax: 24.89,
+    discount: 13.61,
+  },
+  {
+    id: 'ba888c70-bd85-46e3-b69a-9912c287d6d5',
+    country: 'Russia',
+    price: 499.04,
+    tax: 17.78,
+    discount: 33.98,
+  },
+  {
+    id: '542d51be-c32e-41d3-bca0-d3c5daca6673',
+    country: 'Mexico',
+    price: 854.58,
+    tax: 9.66,
+    discount: 3.19,
+  },
+  {
+    id: '16a611e9-0981-4469-a1ce-f3915487680f',
+    country: 'France',
+    price: 126.41,
+    tax: 19.56,
+    discount: 33.5,
+  },
+  {
+    id: '5a97b0f1-3053-40eb-b235-fbc20707ca5b',
+    country: 'USA',
+    price: 465.96,
+    tax: 19.14,
+    discount: 17.76,
+  },
+  {
+    id: '2afbb690-ee72-4b34-9857-b16c8e160f27',
+    country: 'Norway',
+    price: 91.85,
+    tax: 19.4,
+    discount: 33.42,
+  },
+  {
+    id: '796b6291-8db9-43ba-863f-91193af94eec',
+    country: 'Germany',
+    price: 673.11,
+    tax: 20.28,
+    discount: 45.77,
+  },
+  {
+    id: '57a827ba-125b-49e3-846e-e768edefe1e7',
+    country: 'USA',
+    price: 822.44,
+    tax: 21.1,
+    discount: 48.9,
+  },
+  {
+    id: 'f6a84b49-ec30-49c1-82da-4589d62ebe0e',
+    country: 'Netherlands',
+    price: 771.57,
+    tax: 19.92,
+    discount: 33.5,
+  },
+  {
+    id: '1b958cf9-0aca-4bcb-afe2-55188b753cf2',
+    country: 'Canada',
+    price: 288.32,
+    tax: 18.23,
+    discount: 36.97,
+  },
+  {
+    id: 'ece0a9a4-d066-4400-9d3a-3136454df4e0',
+    country: 'South Korea',
+    price: 911.82,
+    tax: 22.36,
+    discount: 26.78,
+  },
+  {
+    id: '704c31b2-b72f-4873-ba29-bde26e0b7584',
+    country: 'Spain',
+    price: 348.37,
+    tax: 4.28,
+    discount: 23.47,
+  },
+  {
+    id: 'f2ddcff3-f0cf-4f30-abd2-a78ed0b510e4',
+    country: 'Russia',
+    price: 781.64,
+    tax: 12.1,
+    discount: 21.56,
+  },
+  {
+    id: '07ad8172-2345-484f-bed6-fcbc8a013ae9',
+    country: 'Norway',
+    price: 329.0,
+    tax: 17.0,
+    discount: 49.56,
+  },
+  {
+    id: '8793db11-695d-45db-8859-9941a6d4faa9',
+    country: 'Netherlands',
+    price: 583.69,
+    tax: 0.16,
+    discount: 49.4,
+  },
+  {
+    id: '36ec6234-0a25-4b1e-99eb-9c4dbf961264',
+    country: 'Germany',
+    price: 151.66,
+    tax: 24.4,
+    discount: 3.45,
+  },
+  {
+    id: '75950ad7-c19f-48a5-8bb8-28e189fe1d5c',
+    country: 'Italy',
+    price: 389.55,
+    tax: 1.0,
+    discount: 17.48,
+  },
+  {
+    id: '4cfb6bb1-cd34-4876-b665-ce2aac85a583',
+    country: 'Mexico',
+    price: 641.99,
+    tax: 18.29,
+    discount: 22.03,
+  },
+  {
+    id: '0ad452e0-a49d-4f5f-b765-d9466749066a',
+    country: 'Australia',
+    price: 27.97,
+    tax: 23.87,
+    discount: 1.96,
+  },
+  {
+    id: '893e0eee-1166-4797-860f-a493026a35e9',
+    country: 'Russia',
+    price: 699.92,
+    tax: 17.37,
+    discount: 2.75,
+  },
+  {
+    id: '9232bd68-3e68-4c6d-8d6a-53e8c7cb7abc',
+    country: 'China',
+    price: 514.05,
+    tax: 2.78,
+    discount: 2.22,
+  },
+  {
+    id: '32375c1d-fc8a-43f7-9b0f-40ebb05f69c3',
+    country: 'UK',
+    price: 388.83,
+    tax: 14.91,
+    discount: 26.45,
+  },
+  {
+    id: 'bda46595-aa88-483d-995e-0fa92e46e698',
+    country: 'Canada',
+    price: 830.41,
+    tax: 21.29,
+    discount: 15.46,
+  },
+  {
+    id: '2f1f0ace-fdab-4691-b130-2396728b337e',
+    country: 'Russia',
+    price: 225.25,
+    tax: 22.8,
+    discount: 10.36,
+  },
+  {
+    id: '9f2202a1-b016-43e5-a132-cc47d4662f4f',
+    country: 'USA',
+    price: 515.03,
+    tax: 21.51,
+    discount: 0.31,
+  },
+  {
+    id: '2f8a7eab-b64a-449e-8663-20d3ac19652e',
+    country: 'South Korea',
+    price: 313.73,
+    tax: 1.87,
+    discount: 40.86,
+  },
+  {
+    id: 'a138596f-475f-49ed-a587-fa5b357dea98',
+    country: 'Italy',
+    price: 176.2,
+    tax: 21.84,
+    discount: 14.48,
+  },
+  {
+    id: '9e57f377-3b62-42c7-9453-ae00bf8b5ff7',
+    country: 'Australia',
+    price: 829.53,
+    tax: 13.69,
+    discount: 8.74,
+  },
+  {
+    id: '307896aa-c8ec-43d1-9e8f-971820dab26c',
+    country: 'India',
+    price: 254.35,
+    tax: 1.98,
+    discount: 37.25,
+  },
+  {
+    id: 'c60441d4-9a4f-4f37-875c-0c1ea04ff95d',
+    country: 'South Africa',
+    price: 273.98,
+    tax: 1.08,
+    discount: 1.72,
+  },
+  {
+    id: '29c30075-2dd3-4bc7-a127-cd4065a8d9e6',
+    country: 'South Korea',
+    price: 26.92,
+    tax: 13.98,
+    discount: 29.95,
+  },
+  {
+    id: 'dd4d5455-2e0f-4e32-b48d-06f20b91a020',
+    country: 'Netherlands',
+    price: 202.29,
+    tax: 20.34,
+    discount: 48.25,
+  },
+  {
+    id: 'b1b36b59-9d5e-4c04-971c-28a3e9b20ecb',
+    country: 'Japan',
+    price: 370.15,
+    tax: 12.5,
+    discount: 1.73,
+  },
+  {
+    id: '18dd3a96-df57-4b07-bb93-87eb5fbedc32',
+    country: 'France',
+    price: 413.47,
+    tax: 7.98,
+    discount: 26.65,
+  },
+  {
+    id: 'add8b930-fc3e-464b-82c8-23b885b3107e',
+    country: 'India',
+    price: 844.84,
+    tax: 12.58,
+    discount: 9.32,
+  },
+  {
+    id: 'e0d5a7f3-c9e3-4e33-b96c-994a8c9c4268',
+    country: 'Mexico',
+    price: 196.99,
+    tax: 1.93,
+    discount: 33.64,
+  },
+  {
+    id: '41a4ec3f-6896-4f42-937b-423adbf33fe0',
+    country: 'Brazil',
+    price: 23.38,
+    tax: 1.12,
+    discount: 25.8,
+  },
+  {
+    id: '2f3b21ab-0f53-4513-b93a-49a51b808b6d',
+    country: 'Spain',
+    price: 348.83,
+    tax: 5.15,
+    discount: 22.16,
+  },
+  {
+    id: 'bbf2387e-35ab-4bc7-9262-8d3c6c50706d',
+    country: 'New Zealand',
+    price: 185.01,
+    tax: 8.92,
+    discount: 37.17,
+  },
+  {
+    id: 'd4552e2a-3a96-4084-9304-67c0a18889ff',
+    country: 'Italy',
+    price: 43.5,
+    tax: 22.71,
+    discount: 13.82,
+  },
+  {
+    id: '197f9bb7-abb4-4806-b40b-7386660fcbb4',
+    country: 'India',
+    price: 655.65,
+    tax: 18.18,
+    discount: 40.35,
+  },
+  {
+    id: 'caee1992-d625-43cf-88db-f8d3e74f96b1',
+    country: 'UK',
+    price: 265.54,
+    tax: 2.59,
+    discount: 23.67,
+  },
+  {
+    id: '0db80996-cd3e-46f1-9a14-295214f8a597',
+    country: 'China',
+    price: 321.94,
+    tax: 7.94,
+    discount: 15.19,
+  },
+  {
+    id: '9b1a7357-573a-439b-99a1-f96d0a75dbf1',
+    country: 'Canada',
+    price: 649.0,
+    tax: 14.16,
+    discount: 38.62,
+  },
+  {
+    id: '17fbbab6-6224-487f-a120-2901fd4b1076',
+    country: 'South Africa',
+    price: 201.31,
+    tax: 4.87,
+    discount: 17.03,
+  },
+  {
+    id: '2d38b07a-1ad6-4d59-9cf4-8df7261154f2',
+    country: 'China',
+    price: 128.56,
+    tax: 2.65,
+    discount: 6.94,
+  },
+  {
+    id: '2e5a5122-567e-45c7-8e93-c2201d3de35e',
+    country: 'Brazil',
+    price: 803.6,
+    tax: 11.86,
+    discount: 24.17,
+  },
+  {
+    id: '01936e9a-5c3e-4066-95d4-5825e7be1bc5',
+    country: 'France',
+    price: 402.83,
+    tax: 16.93,
+    discount: 40.58,
+  },
+  {
+    id: 'b389ae22-ae21-4ff5-9d90-5726b9c1038f',
+    country: 'Norway',
+    price: 218.74,
+    tax: 13.06,
+    discount: 34.36,
+  },
+  {
+    id: '99d9f8ec-7c8f-4f8d-83f3-4aaba3dd5fe1',
+    country: 'Russia',
+    price: 292.01,
+    tax: 12.67,
+    discount: 3.29,
+  },
+  {
+    id: '9da27a8e-3258-4ec5-8f19-34e79d755674',
+    country: 'New Zealand',
+    price: 887.08,
+    tax: 9.17,
+    discount: 47.83,
+  },
+  {
+    id: '63b220b8-80e0-46d5-8b73-9cb7a7d201c6',
+    country: 'India',
+    price: 530.98,
+    tax: 2.13,
+    discount: 16.66,
+  },
+  {
+    id: 'eead89cc-5eeb-44d6-8e89-978d637f4984',
+    country: 'Italy',
+    price: 563.91,
+    tax: 19.78,
+    discount: 12.53,
+  },
+  {
+    id: '116bde9b-35b3-4d07-90b4-7db716d4b36c',
+    country: 'Norway',
+    price: 147.97,
+    tax: 12.81,
+    discount: 26.16,
+  },
+  {
+    id: 'd5e44dff-67c1-4ee2-94e2-15861e5ea554',
+    country: 'Brazil',
+    price: 953.99,
+    tax: 0.92,
+    discount: 5.47,
+  },
+  {
+    id: 'be5284e3-09c1-4e38-9e3b-b5cc49b3be63',
+    country: 'France',
+    price: 184.66,
+    tax: 17.06,
+    discount: 8.57,
+  },
+  {
+    id: 'ad666b5e-93f3-4960-89a7-8f4a9c931d8d',
+    country: 'Mexico',
+    price: 76.76,
+    tax: 22.17,
+    discount: 6.22,
+  },
+  {
+    id: '08a4c1f3-045d-42ca-8ce4-bc22dd5f3839',
+    country: 'USA',
+    price: 874.53,
+    tax: 11.99,
+    discount: 47.92,
+  },
+  {
+    id: 'b407f110-0ca5-48ef-957e-c10dbd77fabb',
+    country: 'South Korea',
+    price: 396.3,
+    tax: 11.63,
+    discount: 20.15,
+  },
+  {
+    id: '95ffae7c-4978-4872-a621-88d07c685c6a',
+    country: 'China',
+    price: 457.85,
+    tax: 1.03,
+    discount: 11.91,
+  },
+  {
+    id: '9d5e1f77-e2bc-4e70-9f00-4ab2692a946d',
+    country: 'Japan',
+    price: 822.96,
+    tax: 11.2,
+    discount: 17.53,
+  },
+  {
+    id: '8473cde8-0b8a-44e1-93b3-b7e0e62ef56c',
+    country: 'Canada',
+    price: 601.38,
+    tax: 17.67,
+    discount: 26.5,
+  },
+  {
+    id: 'a58b18e4-c8ca-4752-9b58-daf9ef5cd7f4',
+    country: 'South Africa',
+    price: 207.02,
+    tax: 22.85,
+    discount: 14.08,
+  },
+  {
+    id: 'c6d60e0c-8ef6-4b9d-a4d2-b22a4c6d06c7',
+    country: 'Spain',
+    price: 141.1,
+    tax: 16.67,
+    discount: 47.06,
+  },
+  {
+    id: 'bc1b6117-1c05-40ec-9b83-7e50c3bfb95c',
+    country: 'Mexico',
+    price: 255.49,
+    tax: 0.6,
+    discount: 36.29,
+  },
+  {
+    id: '87b6f255-fb7e-4e02-933d-9f4debb6fa69',
+    country: 'Japan',
+    price: 77.17,
+    tax: 2.58,
+    discount: 8.95,
+  },
+  {
+    id: '4d21ad85-eab6-404f-bc99-576a46560a4b',
+    country: 'UK',
+    price: 647.35,
+    tax: 1.69,
+    discount: 7.51,
+  },
+]


Test patch:
diff --git a/packages/eds-core-react/src/components/Table/Table.test.tsx b/packages/eds-core-react/src/components/Table/Table.test.tsx
index 2a517222..02ee8a01 100644
--- a/packages/eds-core-react/src/components/Table/Table.test.tsx
+++ b/packages/eds-core-react/src/components/Table/Table.test.tsx
@@ -1,12 +1,50 @@
 /* eslint-disable no-undef */
-import { render, screen } from '@testing-library/react'
+import { render, screen, within } from '@testing-library/react'
 import { axe } from 'jest-axe'
 import { Table } from '.'
 import styled from 'styled-components'
 import { tableCell as dataCellToken } from './DataCell/DataCell.tokens'
 import { token as headerCellToken } from './HeaderCell/HeaderCell.tokens'
 
-const { Caption, Cell, Head, Row, Body } = Table
+const { Caption, Cell, Head, Row, Body, Foot } = Table
+
+const RenderFooterTable = ({ sticky = false }: { sticky?: boolean }) => {
+  return (
+    <Table>
+      <Head>
+        <Row>
+          <Cell>Country</Cell>
+          <Cell>Tax</Cell>
+          <Cell>Discount</Cell>
+        </Row>
+      </Head>
+      <Body>
+        <Row>
+          <Cell>Italy</Cell>
+          <Cell>42</Cell>
+          <Cell>12</Cell>
+        </Row>
+        <Row>
+          <Cell>Norway</Cell>
+          <Cell>50</Cell>
+          <Cell>10</Cell>
+        </Row>
+        <Row>
+          <Cell>Swedend</Cell>
+          <Cell>32</Cell>
+          <Cell>15</Cell>
+        </Row>
+      </Body>
+      <Foot data-testid="footer" sticky={sticky}>
+        <Row>
+          <Cell>Total</Cell>
+          <Cell>54</Cell>
+          <Cell>4</Cell>
+        </Row>
+      </Foot>
+    </Table>
+  )
+}
 
 describe('Caption', () => {
   it('Renders a caption with provided text', () => {
@@ -296,4 +334,22 @@ describe('Table', () => {
     )
     expect(headerCell2).toHaveStyleRule('border-color', borderBottomColor)
   })
+  it('Renders a table with fixed footer if <Foot> is provided', () => {
+    render(<RenderFooterTable sticky />)
+    const foot = screen.getByTestId('footer') // Assert Footer is available in the document
+    expect(foot).toBeInTheDocument()
+    const thElements = within(foot).getAllByRole('columnheader')
+    thElements.forEach((th) => {
+      expect(th).toHaveStyle('position: sticky') // Ensure this one is sticky
+    })
+  })
+  it('Renders a table with footer ( not fixed ) if <Foot> is provided', () => {
+    render(<RenderFooterTable />)
+    const foot = screen.getByTestId('footer') // Assert Footer is available in the document
+    expect(foot).toBeInTheDocument()
+    const thElements = within(foot).getAllByRole('columnheader')
+    thElements.forEach((th) => {
+      expect(th).not.toHaveStyle('position: sticky') // Ensure this one is not sticky
+    })
+  })
 })
diff --git a/packages/eds-data-grid-react/src/tests/EdsDataGrid.test.tsx b/packages/eds-data-grid-react/src/tests/EdsDataGrid.test.tsx
index 02a1b658..c8a61956 100644
--- a/packages/eds-data-grid-react/src/tests/EdsDataGrid.test.tsx
+++ b/packages/eds-data-grid-react/src/tests/EdsDataGrid.test.tsx
@@ -136,6 +136,37 @@ describe('EdsDataGrid', () => {
         expect(window.getComputedStyle(column).position).toBe('sticky')
       })
     })
+
+    /**
+     * Footer
+     */
+    it('should not render footer by default when enableFooter is not provided', () => {
+      render(<EdsDataGrid columns={columns} rows={data} />)
+      const foot = screen.queryByTestId('eds-grid-footer')
+      expect(foot).toBeNull() // Assert Footer is not available in the document
+    })
+
+    it('should render footer when `enableFooter` is provided', () => {
+      render(<EdsDataGrid columns={columns} rows={data} enableFooter />)
+      const foot = screen.getByTestId('eds-grid-footer')
+      expect(foot).toBeTruthy() // Assert Footer is not available in the document
+      const thElements = within(foot).getAllByRole('columnheader')
+      thElements.forEach((th) => {
+        expect(window.getComputedStyle(th).position).not.toBe('sticky')
+      })
+    })
+
+    it('should render sticky footer when `enableFooter` & `stickyFooter` provided', () => {
+      render(
+        <EdsDataGrid columns={columns} rows={data} enableFooter stickyFooter />,
+      )
+      const foot = screen.getByTestId('eds-grid-footer')
+      expect(foot).toBeTruthy() // Assert Footer is not available in the document
+      const thElements = within(foot).getAllByRole('columnheader')
+      thElements.forEach((th) => {
+        expect(window.getComputedStyle(th).position).toBe('sticky')
+      })
+    })
   })
 
   describe('Sorting', () => {
@@ -390,10 +421,13 @@ describe('EdsDataGrid', () => {
       const cellStyle = () => ({ backgroundColor: 'red' })
       const rowStyle = () => ({ backgroundColor: 'blue' })
       const headerStyle = () => ({ backgroundColor: 'green' })
+      const footerStyle = () => ({ backgroundColor: 'yellow', color: 'black' })
       render(
         <EdsDataGrid
+          enableFooter // `enableFooter` will show footer
           cellStyle={cellStyle}
           headerStyle={headerStyle}
+          footerStyle={footerStyle}
           rowStyle={rowStyle}
           columns={columns}
           rows={data}
@@ -407,18 +441,28 @@ describe('EdsDataGrid', () => {
       expect(screen.getAllByRole('columnheader')[0].style.backgroundColor).toBe(
         'green',
       )
+      const foot = screen.getByTestId('eds-grid-footer')
+      expect(foot).toBeTruthy() // Assert Footer is available in the document
+      const thElements = within(foot).getAllByRole('columnheader')
+      thElements.forEach((th) => {
+        expect(th.style.backgroundColor).toBe('yellow')
+        expect(th.style.color).toBe('black')
+      })
     })
 
-    it('should apply classes to the table', () => {
+    it('should apply classes to the table + footer enabled', () => {
       const cellClass = () => 'cell-class'
       const rowClass = () => 'row-class'
       const headerClass = () => 'header-class'
+      const footerClass = () => 'footer-class'
 
       render(
         <EdsDataGrid
+          enableFooter
           cellClass={cellClass}
           rowClass={rowClass}
           headerClass={headerClass}
+          footerClass={footerClass}
           columns={columns}
           rows={data}
         />,
@@ -429,6 +473,12 @@ describe('EdsDataGrid', () => {
       expect(firstCell.classList.contains('cell-class')).toBeTruthy()
       const firstHeader = screen.getAllByRole('columnheader')[0]
       expect(firstHeader.classList.contains('header-class')).toBeTruthy()
+      const foot = screen.getByTestId('eds-grid-footer')
+      expect(foot).toBeTruthy() // Assert Footer is available in the document
+      const thElements = within(foot).getAllByRole('columnheader')
+      thElements.forEach((th) => {
+        expect(th.classList.contains('footer-class')).toBeTruthy()
+      })
     })
   })
 })
diff --git a/packages/eds-data-grid-react/src/tests/Filter.test.tsx b/packages/eds-data-grid-react/src/tests/Filter.test.tsx
index 4f7e0ed4..79c4cf86 100644
--- a/packages/eds-data-grid-react/src/tests/Filter.test.tsx
+++ b/packages/eds-data-grid-react/src/tests/Filter.test.tsx
@@ -62,6 +62,7 @@ describe('Filter', () => {
         <TableProvider
           enableSorting={false}
           stickyHeader={true}
+          stickyFooter={false}
           enableColumnFiltering={true}
           table={table}
         >
@@ -75,6 +76,7 @@ describe('Filter', () => {
         <TableProvider
           enableSorting={false}
           stickyHeader={true}
+          stickyFooter={false}
           enableColumnFiltering={true}
           table={table}
         >
@@ -89,6 +91,7 @@ describe('Filter', () => {
         <TableProvider
           enableSorting={false}
           stickyHeader={true}
+          stickyFooter={false}
           enableColumnFiltering={true}
           table={table}
         >
@@ -106,6 +109,7 @@ describe('Filter', () => {
         <TableProvider
           enableSorting={false}
           stickyHeader={true}
+          stickyFooter={false}
           enableColumnFiltering={true}
           table={table}
         >
@@ -136,6 +140,7 @@ describe('Filter', () => {
         <TableProvider
           enableSorting={false}
           stickyHeader={true}
+          stickyFooter={false}
           enableColumnFiltering={true}
           table={table}
         >
@@ -158,6 +163,7 @@ describe('Filter', () => {
         <TableProvider
           enableSorting={false}
           stickyHeader={true}
+          stickyFooter={false}
           enableColumnFiltering={true}
           table={table}
         >
@@ -186,6 +192,7 @@ describe('Filter', () => {
         <TableProvider
           enableSorting={false}
           stickyHeader={true}
+          stickyFooter={false}
           enableColumnFiltering={true}
           table={table}
         >
@@ -218,6 +225,7 @@ describe('Filter', () => {
         <TableProvider
           enableSorting={false}
           stickyHeader={true}
+          stickyFooter={false}
           enableColumnFiltering={true}
           table={table}
         >
@@ -225,7 +233,7 @@ describe('Filter', () => {
         </TableProvider>,
       )
       openPopover(baseElement)
-      // eslint complains about unneccessary cast, but HTMLElement != HTMLInputElement
+      // eslint complains about unnecessary cast, but HTMLElement != HTMLInputElement
       // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
       const inputs = within(baseElement).getAllByRole(
         'spinbutton',
@@ -256,6 +264,7 @@ describe('Filter', () => {
         <TableProvider
           enableSorting={false}
           stickyHeader={true}
+          stickyFooter={false}
           enableColumnFiltering={true}
           table={table}
         >