Repository: SonarSource/echoes-react
Pull request #333: ECHOES-731 New Table component
Author: login: jeremy-davis-sonarsource
Created: 2025-05-23T07:30:46Z | Merged: 2025-05-27T14:36:25Z

[ECHOES-731](https://sonarsource.atlassian.net/browse/ECHOES-731)



[ECHOES-731]: https://sonarsource.atlassian.net/browse/ECHOES-731?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ


Diff:
diff --git a/design-tokens/tokens/layer3/base.json b/design-tokens/tokens/layer3/base.json
index 818caa4ac..be50b5a23 100644
--- a/design-tokens/tokens/layer3/base.json
+++ b/design-tokens/tokens/layer3/base.json
@@ -134,6 +134,19 @@
           }
         }
       }
+    },
+    "table": {
+      "sizes": {
+        "row": {
+          "minWidth": {
+            "default": {
+              "value": "53px",
+              "type": "sizing",
+              "description": "This is the minimum size we want for the row, regardless of the padding. It includes the 1px border, and as such must be used with box-sizing: border-box."
+            }
+          }
+        }
+      }
     }
   }
 }
\ No newline at end of file
diff --git a/design-tokens/tokens/layer3/dark.json b/design-tokens/tokens/layer3/dark.json
index c9a94e7e5..28a4fbffe 100644
--- a/design-tokens/tokens/layer3/dark.json
+++ b/design-tokens/tokens/layer3/dark.json
@@ -552,6 +552,18 @@
           }
         }
       }
+    },
+    "table": {
+      "colors": {
+        "background": {
+          "header": {
+            "default": {
+              "value": "{echoes.color.grey.500}",
+              "type": "color"
+            }
+          }
+        }
+      }
     }
   }
 }
\ No newline at end of file
diff --git a/design-tokens/tokens/layer3/light.json b/design-tokens/tokens/layer3/light.json
index 1d5a0aefc..e3a6c5d3b 100644
--- a/design-tokens/tokens/layer3/light.json
+++ b/design-tokens/tokens/layer3/light.json
@@ -417,6 +417,18 @@
           }
         }
       }
+    },
+    "table": {
+      "colors": {
+        "background": {
+          "header": {
+            "default": {
+              "value": "{echoes.color.grey.40}",
+              "type": "color"
+            }
+          }
+        }
+      }
     }
   }
 }
\ No newline at end of file
diff --git a/src/components/icons/IconArrowDown.tsx b/src/components/icons/IconArrowDown.tsx
new file mode 100644
index 000000000..4751470eb
--- /dev/null
+++ b/src/components/icons/IconArrowDown.tsx
@@ -0,0 +1,32 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { forwardRef } from 'react';
+import { IconMaterialWrapper, IconProps } from './IconWrapper';
+
+export const IconArrowDown = forwardRef<HTMLSpanElement, IconProps>((props, ref) => {
+  // This is Material Symbols' "arrow_downward" icon
+  return (
+    <IconMaterialWrapper {...props} ref={ref}>
+      &#xE5DB;
+    </IconMaterialWrapper>
+  );
+});
+IconArrowDown.displayName = 'IconArrowDown';
diff --git a/src/components/icons/IconArrowUp.tsx b/src/components/icons/IconArrowUp.tsx
new file mode 100644
index 000000000..02997e165
--- /dev/null
+++ b/src/components/icons/IconArrowUp.tsx
@@ -0,0 +1,32 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { forwardRef } from 'react';
+import { IconMaterialWrapper, IconProps } from './IconWrapper';
+
+export const IconArrowUp = forwardRef<HTMLSpanElement, IconProps>((props, ref) => {
+  // This is Material Symbols' "arrow_upward" icon
+  return (
+    <IconMaterialWrapper {...props} ref={ref}>
+      &#xE5D8;
+    </IconMaterialWrapper>
+  );
+});
+IconArrowUp.displayName = 'IconArrowUp';
diff --git a/src/components/icons/index.ts b/src/components/icons/index.ts
index 7f2ab354b..1288d9535 100644
--- a/src/components/icons/index.ts
+++ b/src/components/icons/index.ts
@@ -22,9 +22,11 @@ import './icons.css';
 
 export type { IconProps } from './IconWrapper';
 
+export { IconArrowDown } from './IconArrowDown';
 export { IconArrowDownRight } from './IconArrowDownRight';
 export { IconArrowLeft } from './IconArrowLeft';
 export { IconArrowRight } from './IconArrowRight';
+export { IconArrowUp } from './IconArrowUp';
 export { IconArrowUpRight } from './IconArrowUpRight';
 export { IconBell } from './IconBell';
 export { IconBranch } from './IconBranch';
diff --git a/src/components/index.ts b/src/components/index.ts
index d69428b6c..517502c52 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -38,6 +38,7 @@ export * from './popover';
 export * from './radio-button-group';
 export * from './select';
 export * from './spinner';
+export * from './table';
 export * from './text-area';
 export * from './text-input';
 export * from './toggle-button-group';
diff --git a/src/components/table/Table.tsx b/src/components/table/Table.tsx
new file mode 100644
index 000000000..2015715d4
--- /dev/null
+++ b/src/components/table/Table.tsx
@@ -0,0 +1,54 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import classNames from 'classnames';
+import { forwardRef } from 'react';
+import { StyledTable } from './TableStyles';
+import { TableProps, TableVariety } from './TableTypes';
+
+export const TableRoot = forwardRef<HTMLTableElement, TableProps>((props, ref) => {
+  const {
+    ariaLabel,
+    ariaLabelledBy,
+    className,
+    children,
+    gridTemplate,
+    variety = TableVariety.Ghost,
+    ...radixProps
+  } = props;
+
+  return (
+    <StyledTable
+      aria-label={ariaLabel}
+      aria-labelledby={ariaLabelledBy}
+      className={classNames(
+        { 'table-variety-surface': variety === TableVariety.Surface },
+        className,
+      )}
+      gridTemplate={gridTemplate}
+      ref={ref}
+      variety={variety}
+      {...radixProps}>
+      {children}
+    </StyledTable>
+  );
+});
+
+TableRoot.displayName = 'Table';
diff --git a/src/components/table/TableCellBadge.tsx b/src/components/table/TableCellBadge.tsx
new file mode 100644
index 000000000..a30c0d45d
--- /dev/null
+++ b/src/components/table/TableCellBadge.tsx
@@ -0,0 +1,39 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { forwardRef } from 'react';
+import { Badge, BadgeProps } from '../badges';
+import { StyledTableCell } from './TableStyles';
+
+export interface TableCellBadgeProps extends BadgeProps {
+  cellClassName?: string;
+}
+
+export const TableCellBadge = forwardRef<HTMLButtonElement, TableCellBadgeProps>((props, ref) => {
+  const { cellClassName, ...badgeProps } = props;
+
+  return (
+    <StyledTableCell className={cellClassName}>
+      <Badge {...badgeProps} ref={ref} />
+    </StyledTableCell>
+  );
+});
+
+TableCellBadge.displayName = 'TableCellBadge';
diff --git a/src/components/table/TableCellButton.tsx b/src/components/table/TableCellButton.tsx
new file mode 100644
index 000000000..8e0167432
--- /dev/null
+++ b/src/components/table/TableCellButton.tsx
@@ -0,0 +1,44 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import styled from '@emotion/styled';
+import { forwardRef } from 'react';
+import { Button, ButtonProps } from '../buttons/Button';
+import { StyledTableCell } from './TableStyles';
+
+export interface TableCellButtonProps extends ButtonProps {
+  cellClassName?: string;
+}
+
+export const TableCellButton = forwardRef<HTMLButtonElement, TableCellButtonProps>((props, ref) => {
+  const { cellClassName, ...buttonProps } = props;
+
+  return (
+    <StyledTableCellButton className={cellClassName}>
+      <Button {...buttonProps} ref={ref} />
+    </StyledTableCellButton>
+  );
+});
+
+TableCellButton.displayName = 'TableCellButton';
+
+const StyledTableCellButton = styled(StyledTableCell)`
+  padding: var(--echoes-dimension-space-100) var(--echoes-dimension-space-200);
+`;
diff --git a/src/components/table/TableCellButtonIcon.tsx b/src/components/table/TableCellButtonIcon.tsx
new file mode 100644
index 000000000..9d59665a8
--- /dev/null
+++ b/src/components/table/TableCellButtonIcon.tsx
@@ -0,0 +1,47 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import styled from '@emotion/styled';
+import { forwardRef } from 'react';
+import { ButtonVariety } from '../buttons';
+import { ButtonIcon, ButtonIconProps } from '../buttons/ButtonIcon';
+import { StyledTableCell } from './TableStyles';
+
+export interface TableCellButtonIconProps extends ButtonIconProps {
+  cellClassName?: string;
+}
+
+export const TableCellButtonIcon = forwardRef<HTMLButtonElement, TableCellButtonIconProps>(
+  (props, ref) => {
+    const { cellClassName, ...buttonProps } = props;
+
+    return (
+      <StyledTableCellButtonIcon className={cellClassName}>
+        <ButtonIcon variety={ButtonVariety.DefaultGhost} {...buttonProps} ref={ref} />
+      </StyledTableCellButtonIcon>
+    );
+  },
+);
+
+TableCellButtonIcon.displayName = 'TableCellButtonIcon';
+
+const StyledTableCellButtonIcon = styled(StyledTableCell)`
+  padding: var(--echoes-dimension-space-100) var(--echoes-dimension-space-200);
+`;
diff --git a/src/components/table/TableCellCheckbox.tsx b/src/components/table/TableCellCheckbox.tsx
new file mode 100644
index 000000000..b4b962020
--- /dev/null
+++ b/src/components/table/TableCellCheckbox.tsx
@@ -0,0 +1,43 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { forwardRef } from 'react';
+import { Checkbox, CheckboxProps } from '../checkbox';
+import { StyledTableCell } from './TableStyles';
+
+export interface Props {
+  cellClassName?: string;
+}
+
+export type TableCellCheckboxProps = Props & CheckboxProps;
+
+export const TableCellCheckbox = forwardRef<HTMLButtonElement, TableCellCheckboxProps>(
+  (props, ref) => {
+    const { cellClassName, ...checkboxProps } = props;
+
+    return (
+      <StyledTableCell className={cellClassName}>
+        <Checkbox {...checkboxProps} ref={ref} />
+      </StyledTableCell>
+    );
+  },
+);
+
+TableCellCheckbox.displayName = 'TableCellCheckbox';
diff --git a/src/components/table/TableCellLink.tsx b/src/components/table/TableCellLink.tsx
new file mode 100644
index 000000000..b73a6d0dd
--- /dev/null
+++ b/src/components/table/TableCellLink.tsx
@@ -0,0 +1,53 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { forwardRef } from 'react';
+import { isDefined } from '~common/helpers/types';
+import { TextNode } from '~types/utils';
+import { LinkStandalone, LinkStandaloneProps } from '../links/LinkStandalone';
+import { Text } from '../typography';
+import { StyledContentWrapper, StyledTableCell } from './TableStyles';
+
+export interface TableCellLinkProps extends LinkStandaloneProps {
+  cellClassName?: string;
+  description?: TextNode;
+}
+
+export const TableCellLink = forwardRef<HTMLAnchorElement, TableCellLinkProps>((props, ref) => {
+  const { cellClassName, description, children, ...linkProps } = props;
+
+  return (
+    <StyledTableCell className={cellClassName} css={{ justifyContent: 'left' }}>
+      <StyledContentWrapper>
+        <LinkStandalone {...linkProps} ref={ref}>
+          {children}
+        </LinkStandalone>
+
+        {isDefined(description) && (
+          <Text isSubdued size="small">
+            {description}
+          </Text>
+        )}
+      </StyledContentWrapper>
+    </StyledTableCell>
+  );
+});
+
+TableCellLink.displayName = 'TableCellLink';
diff --git a/src/components/table/TableCellNumber.tsx b/src/components/table/TableCellNumber.tsx
new file mode 100644
index 000000000..ecbbdda72
--- /dev/null
+++ b/src/components/table/TableCellNumber.tsx
@@ -0,0 +1,57 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { forwardRef } from 'react';
+import { isDefined } from '~common/helpers/types';
+import { TextNode } from '~types/utils';
+import { Text } from '../typography';
+import { StyledContentWrapper, StyledTableCell } from './TableStyles';
+
+export interface TableCellTextProps {
+  className?: string;
+  content: TextNode;
+  description?: TextNode;
+}
+
+export const TableCellNumber = forwardRef<HTMLTableCellElement, TableCellTextProps>(
+  (props, ref) => {
+    const { className, content, description, ...radixProps } = props;
+
+    return (
+      <StyledTableCell
+        className={className}
+        css={{ justifyContent: 'end' }}
+        ref={ref}
+        {...radixProps}>
+        <StyledContentWrapper css={{ alignItems: 'end' }}>
+          {content}
+
+          {isDefined(description) && (
+            <Text isSubdued size="small">
+              {description}
+            </Text>
+          )}
+        </StyledContentWrapper>
+      </StyledTableCell>
+    );
+  },
+);
+
+TableCellNumber.displayName = 'TableCellNumber';
diff --git a/src/components/table/TableCellText.tsx b/src/components/table/TableCellText.tsx
new file mode 100644
index 000000000..954082e31
--- /dev/null
+++ b/src/components/table/TableCellText.tsx
@@ -0,0 +1,58 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { forwardRef, ReactNode } from 'react';
+import { isDefined } from '~common/helpers/types';
+import { TextNode } from '~types/utils';
+import { Text } from '../typography';
+import { StyledContentWrapper, StyledTableCell } from './TableStyles';
+
+export interface TableCellTextProps {
+  className?: string;
+  content: TextNode;
+  description?: TextNode;
+  icon?: ReactNode;
+}
+
+export const TableCellText = forwardRef<HTMLTableCellElement, TableCellTextProps>((props, ref) => {
+  const { className, content, description, icon, ...radixProps } = props;
+
+  return (
+    <StyledTableCell
+      className={className}
+      css={{ justifyContent: 'start' }}
+      ref={ref}
+      {...radixProps}>
+      {icon}
+
+      <StyledContentWrapper>
+        {content}
+
+        {isDefined(description) && (
+          <Text isSubdued size="small">
+            {description}
+          </Text>
+        )}
+      </StyledContentWrapper>
+    </StyledTableCell>
+  );
+});
+
+TableCellText.displayName = 'TableCellText';
diff --git a/src/components/table/TableColumnHeaderCell.tsx b/src/components/table/TableColumnHeaderCell.tsx
new file mode 100644
index 000000000..5f6f75f9b
--- /dev/null
+++ b/src/components/table/TableColumnHeaderCell.tsx
@@ -0,0 +1,130 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import styled from '@emotion/styled';
+import { forwardRef, ReactNode } from 'react';
+import { isDefined } from '~common/helpers/types';
+import { TextNode } from '~types/utils';
+import { Button } from '../buttons';
+import { IconArrowDown, IconArrowUp } from '../icons';
+import { ToggleTip, ToggleTipProps } from '../toggle-tip';
+import { StyledTableColumnHeaderCell } from './TableStyles';
+import { TableCellJustify, TableSortDirection } from './TableTypes';
+
+export interface TableColumnHeaderCellProps {
+  className?: string;
+  justify?: `${TableCellJustify}`;
+  label?: TextNode;
+  onSort?: () => void;
+  sortDirection?: `${TableSortDirection}`;
+  toggleTip?: ToggleTipProps;
+}
+
+function getSortProps(direction?: TableColumnHeaderCellProps['sortDirection']): {
+  aria: React.AriaAttributes['aria-sort'];
+  icon: ReactNode;
+} {
+  switch (direction) {
+    case TableSortDirection.Asc:
+      return {
+        aria: 'ascending',
+        icon: <IconArrowUp />,
+      };
+
+    case TableSortDirection.Desc:
+      return {
+        aria: 'descending',
+        icon: <IconArrowDown />,
+      };
+
+    default:
+      return {
+        aria: undefined,
+        icon: <StyledSortIconPlaceholder />,
+      };
+  }
+}
+
+export const TableColumnHeaderCell = forwardRef<HTMLTableCellElement, TableColumnHeaderCellProps>(
+  (props, ref) => {
+    const {
+      className,
+      justify = TableCellJustify.Start,
+      label,
+      onSort,
+      sortDirection,
+      toggleTip,
+      ...radixProps
+    } = props;
+
+    if (isDefined(onSort)) {
+      const sortProps = getSortProps(sortDirection);
+
+      return (
+        <StyledTableColumnHeaderCell
+          {...radixProps}
+          // ARIA sort is recommended by WCAG
+          // https://www.w3.org/WAI/ARIA/apg/practices/grid-and-table-properties/#indicatingsortorderwitharia-sort
+          aria-sort={sortProps.aria}
+          className={className}
+          css={{
+            justifyContent: justify,
+            padding: 'var(--echoes-dimension-space-100)',
+          }}
+          ref={ref}>
+          <Button onClick={onSort} size="medium" suffix={sortProps.icon} variety="default-ghost">
+            {label}
+          </Button>
+        </StyledTableColumnHeaderCell>
+      );
+    }
+
+    return (
+      <StyledTableColumnHeaderCell
+        {...radixProps}
+        className={className}
+        css={{ justifyContent: justify }}
+        ref={ref}>
+        {label}
+        {toggleTip && <StyledToggleTip {...toggleTip} />}
+      </StyledTableColumnHeaderCell>
+    );
+  },
+);
+
+TableColumnHeaderCell.displayName = 'TableColumnHeaderCell';
+
+/*
+ * This is a placeholder so the header label doesn't 'dance around'
+ * when sorting another column.
+ * It also appears on hover, to hint at the effect of clicking.
+ */
+const StyledSortIconPlaceholder = styled(IconArrowUp)`
+  visibility: hidden;
+
+  ${StyledTableColumnHeaderCell}:hover & {
+    visibility: visible;
+  }
+`;
+
+/* Necessary to play well with 'min-content' */
+const StyledToggleTip = styled(ToggleTip)`
+  flex: 0 0 auto;
+`;
diff --git a/src/components/table/TableColumnHeaderCellCheckbox.tsx b/src/components/table/TableColumnHeaderCellCheckbox.tsx
new file mode 100644
index 000000000..2ea3d53f0
--- /dev/null
+++ b/src/components/table/TableColumnHeaderCellCheckbox.tsx
@@ -0,0 +1,44 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { forwardRef } from 'react';
+import { Checkbox, CheckboxProps } from '../checkbox';
+import { StyledTableColumnHeaderCell } from './TableStyles';
+
+export interface Props {
+  cellClassName?: string;
+}
+
+export type TableColumnHeaderCellCheckboxProps = Props & CheckboxProps;
+
+export const TableColumnHeaderCellCheckbox = forwardRef<
+  HTMLButtonElement,
+  TableColumnHeaderCellCheckboxProps
+>((props, ref) => {
+  const { cellClassName, ...checkboxProps } = props;
+
+  return (
+    <StyledTableColumnHeaderCell className={cellClassName}>
+      <Checkbox {...checkboxProps} ref={ref} />
+    </StyledTableColumnHeaderCell>
+  );
+});
+
+TableColumnHeaderCellCheckbox.displayName = 'TableColumnHeaderCellCheckbox';
diff --git a/src/components/table/TableRow.tsx b/src/components/table/TableRow.tsx
new file mode 100644
index 000000000..f8595a875
--- /dev/null
+++ b/src/components/table/TableRow.tsx
@@ -0,0 +1,37 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import classNames from 'classnames';
+import { ComponentProps, forwardRef } from 'react';
+import { StyledTableRow } from './TableStyles';
+
+export interface TableRowProps extends ComponentProps<'tr'> {
+  selected?: boolean;
+}
+
+export const TableRow = forwardRef<HTMLTableRowElement, TableRowProps>((props, ref) => {
+  const { className, selected, ...htmlProps } = props;
+
+  return (
+    <StyledTableRow {...htmlProps} className={classNames(className, { selected })} ref={ref} />
+  );
+});
+
+TableRow.displayName = 'TableRow';
diff --git a/src/components/table/TableRowHeaderCell.tsx b/src/components/table/TableRowHeaderCell.tsx
new file mode 100644
index 000000000..397871478
--- /dev/null
+++ b/src/components/table/TableRowHeaderCell.tsx
@@ -0,0 +1,35 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { forwardRef } from 'react';
+import { TableCellText, TableCellTextProps } from './TableCellText';
+
+/**
+ * This component is just a thin wrapper around the TextCell, to make it a <th/>
+ */
+export const TableRowHeaderCell = forwardRef<HTMLTableCellElement, TableCellTextProps>(
+  (props, ref) => {
+    // We don't want to expose the `as` prop for the TableCellText
+    // but it does technically accept it, since all props are forwarded
+    return <TableCellText ref={ref} {...props} {...{ as: 'th' }} />;
+  },
+);
+
+TableRowHeaderCell.displayName = 'TableRowHeaderCell';
diff --git a/src/components/table/TableStyles.tsx b/src/components/table/TableStyles.tsx
new file mode 100644
index 000000000..f9e92ffbc
--- /dev/null
+++ b/src/components/table/TableStyles.tsx
@@ -0,0 +1,137 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { css } from '@emotion/react';
+import styled from '@emotion/styled';
+import { TableProps, TableVariety } from './TableTypes';
+
+const TABLE_VARIETY_STYLES = {
+  [TableVariety.Surface]: {
+    '--table-header-background-color': 'var(--echoes-table-colors-background-header-default)',
+    border: 'var(--echoes-border-width-default) solid var(--echoes-color-border-bold)',
+    'border-collapse': 'separate',
+  },
+  [TableVariety.Ghost]: {
+    '--table-header-background-color': 'var(--echoes-color-background-default)',
+    border: 'none',
+    'border-collapse': 'collapse',
+  },
+};
+
+export const StyledTable = styled.table<Required<Pick<TableProps, 'variety' | 'gridTemplate'>>>`
+  display: grid;
+
+  grid-template-columns: ${(props) => props.gridTemplate};
+
+  border-radius: var(--echoes-border-radius-200);
+  border-spacing: 0;
+
+  ${({ variety }) => TABLE_VARIETY_STYLES[variety]}
+`;
+
+export const StyledTableHeader = styled.thead`
+  display: contents;
+`;
+
+export const StyledTableBody = styled.tbody`
+  display: contents;
+`;
+
+export const StyledTableRow = styled.tr`
+  display: contents;
+
+  .table-variety-surface thead &:first-of-type {
+    border-top-left-radius: var(--echoes-border-radius-200);
+    border-top-right-radius: var(--echoes-border-radius-200);
+  }
+
+  .table-variety-surface tbody &:last-of-type {
+    border-bottom-left-radius: var(--echoes-border-radius-200);
+    border-bottom-right-radius: var(--echoes-border-radius-200);
+  }
+`;
+
+export const cellBaseStyle = css`
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  gap: var(--echoes-dimension-space-100);
+
+  box-sizing: border-box;
+  min-height: var(--echoes-table-sizes-row-min-width-default);
+  padding: var(--echoes-dimension-space-100) var(--echoes-dimension-space-200);
+
+  font: var(--echoes-typography-text-default-regular);
+
+  tbody & {
+    border-top: var(--echoes-border-width-default) solid var(--echoes-color-border-weak);
+  }
+
+  .table-variety-surface thead tr:first-child &:first-child {
+    border-top-left-radius: var(--echoes-border-radius-200);
+  }
+
+  .table-variety-surface thead tr:first-child &:last-child {
+    border-top-right-radius: var(--echoes-border-radius-200);
+  }
+
+  .table-variety-surface tbody tr:last-child &:first-child {
+    border-bottom-left-radius: var(--echoes-border-radius-200);
+  }
+
+  .table-variety-surface tbody tr:last-child &:last-child {
+    border-bottom-right-radius: var(--echoes-border-radius-200);
+  }
+`;
+
+export const StyledTableCell = styled.td`
+  ${cellBaseStyle}
+
+  justify-content: center;
+
+  ${StyledTableRow}:hover:not(.selected) & {
+    background-color: var(--echoes-color-background-default-hover);
+  }
+
+  ${StyledTableRow}.selected & {
+    background-color: var(--echoes-color-background-selected-weak-default);
+  }
+
+  ${StyledTableRow}:hover.selected & {
+    background-color: var(--echoes-color-background-selected-weak-hover);
+  }
+`;
+
+export const StyledTableColumnHeaderCell = styled.th`
+  ${cellBaseStyle}
+
+  /* justify-content is variable, defined in TableColumnHeaderCell */
+
+  background-color: var(--table-header-background-color);
+  font: var(--echoes-typography-text-default-semi-bold);
+`;
+
+export const StyledContentWrapper = styled.div`
+  display: flex;
+  flex-direction: column;
+  align-items: start;
+  justify-content: center;
+  flex: 1;
+`;
diff --git a/src/components/table/TableTypes.tsx b/src/components/table/TableTypes.tsx
new file mode 100644
index 000000000..3ce10623a
--- /dev/null
+++ b/src/components/table/TableTypes.tsx
@@ -0,0 +1,55 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { ComponentProps, PropsWithChildren } from 'react';
+
+export enum TableVariety {
+  Surface = 'surface',
+  Ghost = 'ghost',
+}
+
+export enum TableCellJustify {
+  Start = 'start',
+  Center = 'center',
+  End = 'end',
+}
+
+export enum TableSortDirection {
+  Asc = 'asc',
+  Desc = 'desc',
+}
+
+export interface TableBaseProps extends PropsWithChildren<ComponentProps<'table'>> {
+  className?: string;
+  gridTemplate: string;
+  variety?: TableVariety;
+}
+
+export interface TablePropsWithLabel extends TableBaseProps {
+  ariaLabel: string;
+  ariaLabelledBy?: never;
+}
+
+export interface TablePropsWithLabeledBy extends TableBaseProps {
+  ariaLabel?: never;
+  ariaLabelledBy: string;
+}
+
+export type TableProps = TablePropsWithLabel | TablePropsWithLabeledBy;
diff --git a/src/components/table/__tests__/Table-test.tsx b/src/components/table/__tests__/Table-test.tsx
new file mode 100644
index 000000000..9c99603d2
--- /dev/null
+++ b/src/components/table/__tests__/Table-test.tsx
@@ -0,0 +1,138 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { screen } from '@testing-library/react';
+import { MemoryRouter, Route, Routes } from 'react-router-dom';
+import { render } from '~common/helpers/test-utils';
+import { Table } from '..';
+import { IconEdit } from '../../icons';
+
+it('should be accessible', () => {
+  setupTable();
+
+  expect(screen.getByRole('table')).toHaveNoA11yViolations({
+    // We accept non-visible labels
+    rules: { 'empty-table-header': { enabled: false } },
+  });
+});
+
+it('should allow selection', async () => {
+  const onCheckHeader = jest.fn();
+  const onCheckRow = jest.fn();
+  const { user } = setupTable({ onCheckRow, onCheckHeader });
+
+  expect(screen.getByRole('checkbox', { name: 'Select 1' })).toBeChecked();
+
+  await user.click(screen.getByRole('checkbox', { name: 'Select 2' }));
+
+  expect(onCheckRow).toHaveBeenCalledWith('2');
+
+  await user.click(screen.getByRole('checkbox', { name: 'Select all' }));
+
+  expect(onCheckHeader).toHaveBeenCalled();
+});
+
+it('should handle sorting', async () => {
+  const onSort = jest.fn();
+  const { user } = setupTable({ onSort });
+
+  expect(screen.getByRole('columnheader', { name: 'col 1' })).not.toHaveAttribute('aria-sort');
+  expect(screen.getByRole('columnheader', { name: 'col 2' })).toHaveAttribute(
+    'aria-sort',
+    'descending',
+  );
+
+  await user.click(screen.getByRole('button', { name: 'col 1' }));
+
+  expect(onSort).toHaveBeenCalledWith('1');
+});
+
+type RowType = [boolean, string, string, string];
+
+function setupTable(
+  args: {
+    onCheckHeader?: () => void;
+    onCheckRow?: () => void;
+    onSort?: (id: string) => void;
+    rows?: RowType[];
+  } = {},
+) {
+  const {
+    onCheckHeader = jest.fn(),
+    onCheckRow = jest.fn(),
+    onSort,
+    rows = [
+      [true, '1', 'first', 'cool'],
+      [false, '2', 'second', 'beans'],
+    ],
+  } = args;
+
+  return setupWithMemoryRouter(
+    <Table ariaLabel="awesome table" gridTemplate="max-content 1fr auto repeat(4, min-content)">
+      <Table.Header>
+        <Table.Row>
+          <Table.ColumnHeaderCellCheckbox
+            ariaLabel="Select all"
+            checked="indeterminate"
+            onCheck={onCheckHeader}
+          />
+          <Table.ColumnHeaderCell label="col 1" onSort={onSort && (() => onSort('1'))} />
+          <Table.ColumnHeaderCell
+            label="col 2"
+            onSort={onSort && (() => onSort('2'))}
+            sortDirection="desc"
+          />
+          <Table.ColumnHeaderCell label="Status" />
+          <Table.ColumnHeaderCell label="Related" />
+          <Table.ColumnHeaderCell label="Clickable" />
+          <Table.ColumnHeaderCell aria-label="Actions" />
+        </Table.Row>
+      </Table.Header>
+
+      <Table.Body>
+        {rows.map(([checked, id, name, info]) => (
+          <Table.Row key={id}>
+            <Table.CellCheckbox
+              ariaLabel={`Select ${id}`}
+              checked={checked}
+              onCheck={() => onCheckRow(id)}
+            />
+            <Table.RowHeaderCell content={name} />
+            <Table.CellNumber content={info} />
+            <Table.CellBadge variety="highlight">new</Table.CellBadge>
+            <Table.CellLink to="#">Interesting link</Table.CellLink>
+            <Table.CellButton>Click me</Table.CellButton>
+            <Table.CellButtonIcon Icon={IconEdit} ariaLabel={`edit ${name}`} />
+          </Table.Row>
+        ))}
+      </Table.Body>
+    </Table>,
+  );
+}
+
+const setupWithMemoryRouter = (component: JSX.Element, initialEntries = ['/']) => {
+  return render(
+    <MemoryRouter initialEntries={initialEntries}>
+      <Routes>
+        <Route element={component} path="/" />
+      </Routes>
+    </MemoryRouter>,
+  );
+};
diff --git a/src/components/table/index.ts b/src/components/table/index.ts
new file mode 100644
index 000000000..e6f927f15
--- /dev/null
+++ b/src/components/table/index.ts
@@ -0,0 +1,202 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { TableRoot } from './Table';
+import { TableCellBadge } from './TableCellBadge';
+import { TableCellButton } from './TableCellButton';
+import { TableCellButtonIcon } from './TableCellButtonIcon';
+import { TableCellCheckbox } from './TableCellCheckbox';
+import { TableCellLink } from './TableCellLink';
+import { TableCellNumber } from './TableCellNumber';
+import { TableCellText } from './TableCellText';
+import { TableColumnHeaderCell } from './TableColumnHeaderCell';
+import { TableColumnHeaderCellCheckbox } from './TableColumnHeaderCellCheckbox';
+import { TableRow } from './TableRow';
+import { TableRowHeaderCell } from './TableRowHeaderCell';
+import { StyledTableBody, StyledTableCell, StyledTableHeader } from './TableStyles';
+
+export { TableVariety, type TableBaseProps } from './TableTypes';
+
+/**
+ * {@link TableRoot | Table}
+ *
+ * The main Table component for Echoes React.
+ *
+ * This component provides a composable and accessible table structure with a variety of subcomponents
+ * for building complex tables. It exposes several specialized cell and header components.
+ *
+ * It uses CSS grid for its layout, and thus requires the gridTemplate definition.
+ * See the {@link https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-columns | MDN Documentation} for more.
+ *
+ * **Permitted Content**
+ *
+ * - `Table.Header`: The table header section.
+ * - `Table.Body`: The table body section.
+ * - `Table.Row`: A row within the table.
+ * - `Table.RowHeaderCell`: A header cell for a row.
+ * - `Table.ColumnHeaderCell`: A header cell for a column.
+ * - `Table.Cell`: A plain table cell for custom content.
+ * - `Table.CellText`: A cell for displaying text.
+ * - `Table.CellNumber`: A cell for displaying numbers.
+ * - `Table.CellLink`: A cell for displaying links.
+ * - `Table.CellButton`: A cell for displaying buttons.
+ * - `Table.CellBadge`: A cell for displaying badges.
+ * - `Table.CellCheckbox`: A cell for displaying checkboxes.
+ * - `Table.ColumnHeaderCellCheckbox`: A header cell for a checkbox column.
+ *
+ * **example**
+ *
+ * ```tsx
+ * <Table gridTemplate='min-content 1fr auto'>
+ *   <Table.Header>
+ *     <Table.Row>
+ *       <Table.ColumnHeaderCellCheckbox />
+ *       <Table.ColumnHeaderCell>Project</Table.ColumnHeaderCell>
+ *       <Table.ColumnHeaderCell>Status</Table.ColumnHeaderCell>
+ *     </Table.Row>
+ *   </Table.Header>
+ *   <Table.Body>
+ *     <Table.Row>
+ *       <Table.CellCheckbox checked={true} />
+ *       <Table.CellText>Echoes</Table.CellText>
+ *       <Table.CellBadge status="success">Active</Table.CellBadge>
+ *     </Table.Row>
+ *   </Table.Body>
+ * </Table>
+ * ```
+ *
+ * **remarks**
+ *
+ * Use the specialized cell components (`CellText`, `CellNumber`, etc.) for consistent styling and accessibility.
+ * The plain `Cell` component is provided as an escape hatch for custom content and should be used sparingly.
+ *
+ */
+export const Table = Object.assign(TableRoot, {
+  /**
+   * {@link StyledTableHeader | Table.Header}
+   *
+   * This is the table header section. It is required!
+   */
+  Header: StyledTableHeader,
+
+  /**
+   * {@link StyledTableBody | Table.Body}
+   *
+   * This is the table body section. It is required!
+   */
+  Body: StyledTableBody,
+
+  /**
+   * {@link TableRow | Table.Row}
+   *
+   * This is a table row. Wrap your cells in it.
+   * You can make it selectable by defining the `selected` prop as well as adding a {@link TableCellCheckbox | Table.CellCheckbox}.
+   */
+  Row: TableRow,
+
+  /**
+   * {@link TableRowHeaderCell | Table.RowHeaderCell}
+   *
+   * This is a table row header cell. It must have a label or an aria-label.
+   * Each row should have a header cell to identify the item represented in that row.
+   * This will typically be a name, or identifier.
+   */
+  RowHeaderCell: TableRowHeaderCell,
+
+  /**
+   * {@link TableColumnHeaderCell | Table.ColumnHeaderCell}
+   *
+   * This is a table column header cell. It must have a label or an aria-label.
+   * It may have a toggletip to describe its purpose.
+   *
+   * ** Sortable **
+   * Providing an `onSort` callback will turn this cell's contents into a button.
+   * The sorting mechanism must be implemented by you, but providing the sort order will
+   * automatically add an indicator and relevant aria attribute to the header.
+   *
+   */
+  ColumnHeaderCell: TableColumnHeaderCell,
+
+  /**
+   * {@link StyledTableCell | Table.Cell}
+   *
+   * This is a plain cell:
+   * it serves as an escape hatch if you need a cell with custom content
+   *
+   * Only use this as a last resort!
+   */
+  Cell: StyledTableCell,
+
+  /**
+   * {@link TableCellText | Table.CellText}
+   *
+   * This is a simple cell to display text.
+   */
+  CellText: TableCellText,
+
+  /**
+   * {@link TableCellNumber | Table.CellNumber}
+   *
+   * This is a cell to display a number.
+   */
+  CellNumber: TableCellNumber,
+
+  /**
+   * {@link TableCellLink | Table.CellLink}
+   *
+   * This is a cell to display a Link. You may wrap it in a Tooltip if necessary: the Tootlip will be attached to the Link in the cell.
+   */
+  CellLink: TableCellLink,
+
+  /**
+   * {@link TableCellButton | Table.CellButton}
+   *
+   * This is a cell to display a Button. You may use it as a trigger (Dropdown, Tooltip, Popover, ...).
+   */
+  CellButton: TableCellButton,
+
+  /**
+   * {@link TableCellButtonIcon | Table.CellButtonIcon}
+   *
+   * This is a cell to display a ButtonIcon. You may use it as a trigger (Dropdown, Tooltip, Popover, ...).
+   */
+  CellButtonIcon: TableCellButtonIcon,
+
+  /**
+   * {@link TableCellBadge | Table.CellBadge}
+   *
+   * This is a cell to display a Badge. You can make it `isInteractive` and wrap it in a Popover like a regular badge.
+   */
+  CellBadge: TableCellBadge,
+
+  /**
+   * {@link TableCellCheckbox | Table.CellCheckbox}
+   *
+   * This is a cell to display a Checkbox. Primarily used for selectable rows.
+   */
+  CellCheckbox: TableCellCheckbox,
+
+  /**
+   * {@link TableColumnHeaderCellCheckbox | Table.ColumnHeaderCellCheckbox}
+   *
+   * This is a header cell for the checkbox column.
+   */
+  ColumnHeaderCellCheckbox: TableColumnHeaderCellCheckbox,
+});
diff --git a/src/generated/design-tokens-base.css b/src/generated/design-tokens-base.css
index 187dc46ba..f1f61e76e 100644
--- a/src/generated/design-tokens-base.css
+++ b/src/generated/design-tokens-base.css
@@ -248,4 +248,5 @@
   --echoes-sizes-breadcrumbs-max-width-default: 300px;
   --echoes-offset-dropdown-submenu-panel-vertical: 0px;
   --echoes-offset-dropdown-submenu-panel-horizontal: 8px;
+  --echoes-table-sizes-row-min-width-default: 53px; /* This is the minimum size we want for the row, regardless of the padding. It includes the 1px border, and as such must be used with box-sizing: border-box. */
 }
diff --git a/src/generated/design-tokens-base.json b/src/generated/design-tokens-base.json
index f7da48922..fb1cfc64f 100644
--- a/src/generated/design-tokens-base.json
+++ b/src/generated/design-tokens-base.json
@@ -354,6 +354,15 @@
           }
         }
       }
+    },
+    "table": {
+      "sizes": {
+        "row": {
+          "minWidth": {
+            "default": "53px"
+          }
+        }
+      }
     }
   }
 }
diff --git a/src/generated/design-tokens-dark.css b/src/generated/design-tokens-dark.css
index 8e04a5e0a..00fd8fbc1 100644
--- a/src/generated/design-tokens-dark.css
+++ b/src/generated/design-tokens-dark.css
@@ -158,6 +158,7 @@ html[data-echoes-theme='dark'], [data-echoes-theme='dark'] {
   --echoes-form-control-colors-border-default: rgb(166,173,194);
   --echoes-form-control-colors-border-focus: rgb(99,113,146);
   --echoes-form-control-colors-icon-default: rgb(197,205,223);
+  --echoes-table-colors-background-header-default: rgb(62,67,87);
   --echoes-box-shadow-xsmall: 0 1px 4px 0 rgba(8,9,12,0.4);
   --echoes-box-shadow-small: 0 1px 10px 0 rgba(8,9,12,0.4), 0 1px 3px 0 rgba(8,9,12,0.4);
   --echoes-box-shadow-medium: inset 0 2px 15px -2px rgba(8,9,12,0.06), 0 4px 15px -2px rgba(8,9,12,0.5);
diff --git a/src/generated/design-tokens-light.css b/src/generated/design-tokens-light.css
index fe5d38489..3e9853b16 100644
--- a/src/generated/design-tokens-light.css
+++ b/src/generated/design-tokens-light.css
@@ -158,6 +158,7 @@
   --echoes-form-control-colors-border-default: rgb(197,205,223);
   --echoes-form-control-colors-border-focus: rgb(225,230,243);
   --echoes-form-control-colors-icon-default: rgb(99,113,146);
+  --echoes-table-colors-background-header-default: rgb(246,247,251);
   --echoes-box-shadow-xsmall: 0 1px 02px 0 rgba(0,0,0,0.05);
   --echoes-box-shadow-small: 0 1px 25px 0 rgba(29,33,47,0.05), 0 1px 3px 0 rgba(29,33,47,0.05);
   --echoes-box-shadow-medium: 0 2px 15px -2px rgba(29,33,47,0.06), 0 4px 8px -2px rgba(29,33,47,0.1);
diff --git a/src/generated/design-tokens-themed.json b/src/generated/design-tokens-themed.json
index 6d2861b0b..cb0897aeb 100644
--- a/src/generated/design-tokens-themed.json
+++ b/src/generated/design-tokens-themed.json
@@ -136,6 +136,7 @@
   "echoes-form-control-colors-border-default": "rgb(197,205,223)",
   "echoes-form-control-colors-border-focus": "rgb(225,230,243)",
   "echoes-form-control-colors-icon-default": "rgb(99,113,146)",
+  "echoes-table-colors-background-header-default": "rgb(246,247,251)",
   "echoes-box-shadow-xsmall": "0 1px 02px 0 rgba(0,0,0,0.05)",
   "echoes-box-shadow-small": "0 1px 25px 0 rgba(29,33,47,0.05), 0 1px 3px 0 rgba(29,33,47,0.05)",
   "echoes-box-shadow-medium": "0 2px 15px -2px rgba(29,33,47,0.06), 0 4px 8px -2px rgba(29,33,47,0.1)",
diff --git a/src/generated/material-symbols-rounded-optimized.woff2 b/src/generated/material-symbols-rounded-optimized.woff2
index e97327ed2..5651ce5d2 100644
Binary files a/src/generated/material-symbols-rounded-optimized.woff2 and b/src/generated/material-symbols-rounded-optimized.woff2 differ
diff --git a/stories/Table-stories.tsx b/stories/Table-stories.tsx
new file mode 100644
index 000000000..32b6583a5
--- /dev/null
+++ b/stories/Table-stories.tsx
@@ -0,0 +1,254 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import type { Meta, StoryObj } from '@storybook/react';
+import { useCallback, useState } from 'react';
+import { TableProps, TableSortDirection } from 'src/components/table/TableTypes';
+import {
+  BadgeVariety,
+  DropdownMenu,
+  IconMoreVertical,
+  IconWarning,
+  Popover,
+  Table,
+  TableVariety,
+  Tooltip,
+} from '../src';
+import { basicWrapperDecorator } from './helpers/BasicWrapper';
+
+const meta: Meta<typeof Table> = {
+  component: Table,
+  title: 'Echoes/Table',
+  parameters: {
+    controls: { exclude: ['children'] },
+  },
+  argTypes: {
+    variety: { control: { type: 'select' }, options: Object.values(TableVariety) },
+  },
+  decorators: [basicWrapperDecorator],
+};
+
+export default meta;
+
+type Story = StoryObj<typeof Table>;
+
+export const Selectable: Story = {
+  args: {
+    ariaLabel: 'Awesome table',
+    gridTemplate: 'max-content 1fr max-content 1fr 1fr max-content max-content',
+    variety: TableVariety.Surface,
+  },
+  render: (args) => <StateManager {...args} />,
+};
+
+const DATA = [
+  {
+    name: 'Michelangelo',
+    email: 'mikey@sewers.nyc',
+    weapon: 'Nunchaku',
+    age: '13',
+    badge: BadgeVariety.Warning,
+  },
+  {
+    name: 'Leonardo',
+    description: "He's the leader!",
+    email: 'leo@sewers.nyc',
+    weapon: 'Katana',
+    age: '15',
+    ageDescription: 'oldest',
+    badge: BadgeVariety.Info,
+  },
+  {
+    name: 'Donatello',
+    description: 'techie',
+    email: 'donnie@sewers.nyc',
+    weapon: 'Bo',
+    age: '14',
+    badge: BadgeVariety.Highlight,
+  },
+  {
+    name: 'Raphael',
+    email: 'raph@sewers.nyc',
+    emailDescription: "don't bother him!",
+    emailTooltip: 'he can be a bit impatient',
+    weapon: 'Sai',
+    age: '14',
+    badge: BadgeVariety.Danger,
+  },
+];
+
+function getSelectionState(selectedRows: Record<string, boolean>) {
+  const values = Object.values(selectedRows);
+
+  if (values.every((v) => v)) {
+    return true;
+  }
+
+  if (values.every((v) => !v)) {
+    return false;
+  }
+
+  return 'indeterminate';
+}
+
+function StateManager(props: TableProps) {
+  const [selectedRows, setSelectedRows] = useState(
+    DATA.reduce(
+      (result, item) => {
+        result[item.name] = false;
+        return result;
+      },
+      {} as Record<string, boolean>,
+    ),
+  );
+
+  const [sorting, setSorting] = useState<{ column: string; direction?: `${TableSortDirection}` }>({
+    column: '',
+    direction: undefined,
+  });
+
+  const selectionState = getSelectionState(selectedRows);
+
+  const toggleRow = useCallback(
+    (name: string) => {
+      setSelectedRows((selectedRows) => {
+        return { ...selectedRows, [name]: !selectedRows[name] };
+      });
+    },
+    [setSelectedRows],
+  );
+
+  const toggleAll = useCallback(() => {
+    setSelectedRows((selectedRows) => {
+      DATA.forEach((d) => {
+        selectedRows[d.name] = selectionState === 'indeterminate' || !selectionState;
+      });
+
+      return { ...selectedRows };
+    });
+  }, [setSelectedRows, selectionState]);
+
+  const toggleSorting = useCallback(
+    (column: string) => {
+      setSorting((sorting) => {
+        if (sorting.column === column) {
+          return {
+            column,
+            direction: sorting.direction === 'asc' ? 'desc' : 'asc',
+          };
+        }
+
+        return { column, direction: 'asc' };
+      });
+    },
+    [setSorting],
+  );
+
+  return (
+    <Table {...props}>
+      <Table.Header>
+        <Table.Row>
+          <Table.ColumnHeaderCellCheckbox
+            ariaLabel="Select All"
+            checked={getSelectionState(selectedRows)}
+            onCheck={toggleAll}
+          />
+          <Table.ColumnHeaderCell
+            label="Name"
+            toggleTip={{
+              title: 'kowabunga!!!',
+            }}
+          />
+          <Table.ColumnHeaderCell aria-label="Color" />
+          <Table.ColumnHeaderCell label="Email" />
+
+          <Tooltip content="While inspired by medieval Japan, the weapons the turtles use are made from recycled materials found in the NYC sewers">
+            <Table.ColumnHeaderCell
+              label="Weapon"
+              onSort={() => {
+                toggleSorting('weapon');
+              }}
+              sortDirection={sorting.column === 'weapon' ? sorting.direction : undefined}
+            />
+          </Tooltip>
+          <Table.ColumnHeaderCell
+            justify="end"
+            label="Age"
+            onSort={() => {
+              toggleSorting('age');
+            }}
+            sortDirection={sorting.column === 'age' ? sorting.direction : undefined}
+          />
+          <Table.ColumnHeaderCell aria-label="more actions" />
+        </Table.Row>
+      </Table.Header>
+
+      <Table.Body>
+        {DATA.map((turtle) => (
+          <Table.Row key={turtle.name} selected={selectedRows[turtle.name]}>
+            <Table.CellCheckbox
+              ariaLabel={`Select ${turtle.name}`}
+              checked={selectedRows[turtle.name]}
+              onCheck={() => toggleRow(turtle.name)}
+            />
+            <Table.RowHeaderCell content={turtle.name} description={turtle.description} />
+
+            <Popover title="Teenage Mutant Ninja Turtles">
+              <Table.CellBadge isHighContrast isInteractive variety={turtle.badge}>
+                TMNT
+              </Table.CellBadge>
+            </Popover>
+
+            {turtle.emailTooltip ? (
+              <Tooltip content="he can be a bit impatient">
+                <Table.CellLink description={turtle.emailDescription} to="#">
+                  {turtle.email}
+                </Table.CellLink>
+              </Tooltip>
+            ) : (
+              <Table.CellLink description={turtle.emailDescription} to="#">
+                {turtle.email}
+              </Table.CellLink>
+            )}
+
+            <Table.CellText
+              content={turtle.weapon}
+              icon={<IconWarning color="echoes-color-icon-emphasis" />}
+            />
+            <Table.CellNumber content={turtle.age} description={turtle.ageDescription} />
+
+            <DropdownMenu
+              items={
+                <>
+                  <DropdownMenu.ItemLink to="#">check out</DropdownMenu.ItemLink>
+                  <DropdownMenu.ItemButtonDestructive>Delete!</DropdownMenu.ItemButtonDestructive>
+                </>
+              }>
+              <Table.CellButtonIcon
+                Icon={IconMoreVertical}
+                ariaLabel={`more actions for ${turtle.name}`}
+              />
+            </DropdownMenu>
+          </Table.Row>
+        ))}
+      </Table.Body>
+    </Table>
+  );
+}


Reviews:
user: login: david-cho-lerat-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T16:02:18Z
user: login: david-cho-lerat-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T16:03:11Z
user: login: david-cho-lerat-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T16:04:22Z
user: login: david-cho-lerat-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T16:07:04Z
user: login: david-cho-lerat-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T16:09:11Z
user: login: david-cho-lerat-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T16:10:07Z
user: login: david-cho-lerat-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T16:10:37Z
user: login: david-cho-lerat-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T16:11:12Z
user: login: david-cho-lerat-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T16:11:39Z
user: login: david-cho-lerat-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T16:11:48Z
user: login: david-cho-lerat-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T07:44:17Z
user: login: david-cho-lerat-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T07:55:19Z
user: login: david-cho-lerat-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T08:18:09Z
user: login: david-cho-lerat-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T08:18:18Z
user: login: david-cho-lerat-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T08:19:18Z
user: login: david-cho-lerat-sonarsource
body: Amazing work, well done :muscle:  :tada: 

(Just a couple of typos, and the rest of the comments is mostly NPs)
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T08:21:05Z
user: login: jeremy-davis-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T14:09:29Z
user: login: david-cho-lerat-sonarsource
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T14:25:24Z

Review comments:
diff_hunk: @@ -0,0 +1,202 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { TableRoot } from './Table';
+import { TableCellBadge } from './TableCellBadge';
+import { TableCellButton } from './TableCellButton';
+import { TableCellButtonIcon } from './TableCellButtonIcon';
+import { TableCellCheckbox } from './TableCellCheckbox';
+import { TableCellLink } from './TableCellLink';
+import { TableCellNumber } from './TableCellNumber';
+import { TableCellText } from './TableCellText';
+import { TableColumnHeaderCell } from './TableColumnHeaderCell';
+import { TableColumnHeaderCellCheckbox } from './TableColumnHeaderCellCheckbox';
+import { TableRow } from './TableRow';
+import { TableRowHeaderCell } from './TableRowHeaderCell';
+import { StyledTableBody, StyledTableCell, StyledTableHeader } from './TableStyles';
+
+export { TableVariety, type TableBaseProps } from './TableTypes';
+
+/**
+ * {@link TableRoot | Table}
+ *
+ * The main Table component for Echoes React.
+ *
+ * This component provides a composable and accessible table structure with a variety of subcomponents
+ * for building complex tables. It exposes several specialized cell and header components.
+ *
+ * It uses CSS grid for its layout, and thus requires the gridTemplate definition.
+ * See the {@link https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-columns | MDN Documentation} for more.
+ *
+ * **Permitted Content**
+ *
+ * - `Table.Header`: The table header section.
+ * - `Table.Body`: The table body section.
+ * - `Table.Row`: A row within the table.
+ * - `Table.RowHeaderCell`: A header cell for a row.
+ * - `Table.ColumnHeaderCell`: A header cell for a column.
+ * - `Table.Cell`: A plain table cell for custom content.
+ * - `Table.CellText`: A cell for displaying text.
+ * - `Table.CellNumber`: A cell for displaying numbers.
+ * - `Table.CellLink`: A cell for displaying links.
+ * - `Table.CellButton`: A cell for displaying buttons.
+ * - `Table.CellBadge`: A cell for displaying badges.
+ * - `Table.CellCheckbox`: A cell for displaying checkboxes.
+ * - `Table.ColumnHeaderCellCheckbox`: A header cell for a checkbox column.
+ *
+ * **example**
+ *
+ * ```tsx
+ * <Table gridTemplate='min-content 1fr auto'>
+ *   <Table.Header>
+ *     <Table.Row>
+ *       <Table.ColumnHeaderCellCheckbox />
+ *       <Table.ColumnHeaderCell>Project</Table.ColumnHeaderCell>
+ *       <Table.ColumnHeaderCell>Status</Table.ColumnHeaderCell>
+ *     </Table.Row>
+ *   </Table.Header>
+ *   <Table.Body>
+ *     <Table.Row>
+ *       <Table.CellCheckbox checked={true} />
+ *       <Table.CellText>Echoes</Table.CellText>
+ *       <Table.CellBadge status="success">Active</Table.CellBadge>
+ *     </Table.Row>
+ *   </Table.Body>
+ * </Table>
+ * ```
+ *
+ * **remarks**
+ *
+ * Use the specialized cell components (`CellText`, `CellNumber`, etc.) for consistent styling and accessibility.
+ * The plain `Cell` component is provided as an escape hatch for custom content and should be used sparingly.
+ *
+ */
+export const Table = Object.assign(TableRoot, {
+  /**
+   * {@link StyledTableHeader | Table.Header}
+   *
+   * This is the table header section. It is required!
+   */
+  Header: StyledTableHeader,
+
+  /**
+   * {@link StyledTableBody | Table.Body}
+   *
+   * This is the table body section. It is required!
+   */
+  Body: StyledTableBody,
+
+  /**
+   * {@link TableRow | Table.Row}
+   *
+   * This is a table row. Wrap your cells in it.
+   * You can make it selectable by defining the `selected` prop as well as adding a {@link TableCellCheckbox | Table.CellCheckbox}.
+   */
+  Row: TableRow,
+
+  /**
+   * {@link TableRowHeaderCell | Table.RowHeaderCell}
+   *
+   * This is a table row header cell. It must have a label or an aria-label.
+   * Each row should have a header cell to identify the item represented in that row.
+   * This will typically be a name, or identifier.
+   */
+  RowHeaderCell: TableRowHeaderCell,
+
+  /**
+   * {@link TableColumnHeaderCell | Table.ColumnHeaderCell}
+   *
+   * This is a table column header cell. It must have a label or an aria-label.
+   * It may have a toggletip to decribe its purpose.
path: src/components/table/index.ts
user: login: david-cho-lerat-sonarsource
body: ```suggestion
   * It may have a toggletip to describe its purpose.
```
created_at: 2025-05-26T16:02:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,202 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { TableRoot } from './Table';
+import { TableCellBadge } from './TableCellBadge';
+import { TableCellButton } from './TableCellButton';
+import { TableCellButtonIcon } from './TableCellButtonIcon';
+import { TableCellCheckbox } from './TableCellCheckbox';
+import { TableCellLink } from './TableCellLink';
+import { TableCellNumber } from './TableCellNumber';
+import { TableCellText } from './TableCellText';
+import { TableColumnHeaderCell } from './TableColumnHeaderCell';
+import { TableColumnHeaderCellCheckbox } from './TableColumnHeaderCellCheckbox';
+import { TableRow } from './TableRow';
+import { TableRowHeaderCell } from './TableRowHeaderCell';
+import { StyledTableBody, StyledTableCell, StyledTableHeader } from './TableStyles';
+
+export { TableVariety, type TableBaseProps } from './TableTypes';
+
+/**
+ * {@link TableRoot | Table}
+ *
+ * The main Table component for Echoes React.
+ *
+ * This component provides a composable and accessible table structure with a variety of subcomponents
+ * for building complex tables. It exposes several specialized cell and header components.
+ *
+ * It uses CSS grid for its layout, and thus requires the gridTemplate definition.
+ * See the {@link https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-columns | MDN Documentation} for more.
+ *
+ * **Permitted Content**
+ *
+ * - `Table.Header`: The table header section.
+ * - `Table.Body`: The table body section.
+ * - `Table.Row`: A row within the table.
+ * - `Table.RowHeaderCell`: A header cell for a row.
+ * - `Table.ColumnHeaderCell`: A header cell for a column.
+ * - `Table.Cell`: A plain table cell for custom content.
+ * - `Table.CellText`: A cell for displaying text.
+ * - `Table.CellNumber`: A cell for displaying numbers.
+ * - `Table.CellLink`: A cell for displaying links.
+ * - `Table.CellButton`: A cell for displaying buttons.
+ * - `Table.CellBadge`: A cell for displaying badges.
+ * - `Table.CellCheckbox`: A cell for displaying checkboxes.
+ * - `Table.ColumnHeaderCellCheckbox`: A header cell for a checkbox column.
+ *
+ * **example**
+ *
+ * ```tsx
+ * <Table gridTemplate='min-content 1fr auto'>
+ *   <Table.Header>
+ *     <Table.Row>
+ *       <Table.ColumnHeaderCellCheckbox />
+ *       <Table.ColumnHeaderCell>Project</Table.ColumnHeaderCell>
+ *       <Table.ColumnHeaderCell>Status</Table.ColumnHeaderCell>
+ *     </Table.Row>
+ *   </Table.Header>
+ *   <Table.Body>
+ *     <Table.Row>
+ *       <Table.CellCheckbox checked={true} />
+ *       <Table.CellText>Echoes</Table.CellText>
+ *       <Table.CellBadge status="success">Active</Table.CellBadge>
+ *     </Table.Row>
+ *   </Table.Body>
+ * </Table>
+ * ```
+ *
+ * **remarks**
+ *
+ * Use the specialized cell components (`CellText`, `CellNumber`, etc.) for consistent styling and accessibility.
+ * The plain `Cell` component is provided as an escape hatch for custom content and should be used sparingly.
+ *
+ */
+export const Table = Object.assign(TableRoot, {
+  /**
+   * {@link StyledTableHeader | Table.Header}
+   *
+   * This is the table header section. It is required!
+   */
+  Header: StyledTableHeader,
+
+  /**
+   * {@link StyledTableBody | Table.Body}
+   *
+   * This is the table body section. It is required!
+   */
+  Body: StyledTableBody,
+
+  /**
+   * {@link TableRow | Table.Row}
+   *
+   * This is a table row. Wrap your cells in it.
+   * You can make it selectable by defining the `selected` prop as well as adding a {@link TableCellCheckbox | Table.CellCheckbox}.
+   */
+  Row: TableRow,
+
+  /**
+   * {@link TableRowHeaderCell | Table.RowHeaderCell}
+   *
+   * This is a table row header cell. It must have a label or an aria-label.
+   * Each row should have a header cell to identify the item represented in that row.
+   * This will typically be a name, or identifier.
+   */
+  RowHeaderCell: TableRowHeaderCell,
+
+  /**
+   * {@link TableColumnHeaderCell | Table.ColumnHeaderCell}
+   *
+   * This is a table column header cell. It must have a label or an aria-label.
+   * It may have a toggletip to decribe its purpose.
+   *
+   * ** Sortable **
+   * Providing a `onSort` callback will turn this cells' contents into a button.
path: src/components/table/index.ts
user: login: david-cho-lerat-sonarsource
body: ```suggestion
   * Providing an `onSort` callback will turn this cell's contents into a button.
```
created_at: 2025-05-26T16:03:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,202 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { TableRoot } from './Table';
+import { TableCellBadge } from './TableCellBadge';
+import { TableCellButton } from './TableCellButton';
+import { TableCellButtonIcon } from './TableCellButtonIcon';
+import { TableCellCheckbox } from './TableCellCheckbox';
+import { TableCellLink } from './TableCellLink';
+import { TableCellNumber } from './TableCellNumber';
+import { TableCellText } from './TableCellText';
+import { TableColumnHeaderCell } from './TableColumnHeaderCell';
+import { TableColumnHeaderCellCheckbox } from './TableColumnHeaderCellCheckbox';
+import { TableRow } from './TableRow';
+import { TableRowHeaderCell } from './TableRowHeaderCell';
+import { StyledTableBody, StyledTableCell, StyledTableHeader } from './TableStyles';
+
+export { TableVariety, type TableBaseProps } from './TableTypes';
+
+/**
+ * {@link TableRoot | Table}
+ *
+ * The main Table component for Echoes React.
+ *
+ * This component provides a composable and accessible table structure with a variety of subcomponents
+ * for building complex tables. It exposes several specialized cell and header components.
+ *
+ * It uses CSS grid for its layout, and thus requires the gridTemplate definition.
+ * See the {@link https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-columns | MDN Documentation} for more.
+ *
+ * **Permitted Content**
+ *
+ * - `Table.Header`: The table header section.
+ * - `Table.Body`: The table body section.
+ * - `Table.Row`: A row within the table.
+ * - `Table.RowHeaderCell`: A header cell for a row.
+ * - `Table.ColumnHeaderCell`: A header cell for a column.
+ * - `Table.Cell`: A plain table cell for custom content.
+ * - `Table.CellText`: A cell for displaying text.
+ * - `Table.CellNumber`: A cell for displaying numbers.
+ * - `Table.CellLink`: A cell for displaying links.
+ * - `Table.CellButton`: A cell for displaying buttons.
+ * - `Table.CellBadge`: A cell for displaying badges.
+ * - `Table.CellCheckbox`: A cell for displaying checkboxes.
+ * - `Table.ColumnHeaderCellCheckbox`: A header cell for a checkbox column.
+ *
+ * **example**
+ *
+ * ```tsx
+ * <Table gridTemplate='min-content 1fr auto'>
+ *   <Table.Header>
+ *     <Table.Row>
+ *       <Table.ColumnHeaderCellCheckbox />
+ *       <Table.ColumnHeaderCell>Project</Table.ColumnHeaderCell>
+ *       <Table.ColumnHeaderCell>Status</Table.ColumnHeaderCell>
+ *     </Table.Row>
+ *   </Table.Header>
+ *   <Table.Body>
+ *     <Table.Row>
+ *       <Table.CellCheckbox checked={true} />
+ *       <Table.CellText>Echoes</Table.CellText>
+ *       <Table.CellBadge status="success">Active</Table.CellBadge>
+ *     </Table.Row>
+ *   </Table.Body>
+ * </Table>
+ * ```
+ *
+ * **remarks**
+ *
+ * Use the specialized cell components (`CellText`, `CellNumber`, etc.) for consistent styling and accessibility.
+ * The plain `Cell` component is provided as an escape hatch for custom content and should be used sparingly.
+ *
+ */
+export const Table = Object.assign(TableRoot, {
+  /**
+   * {@link StyledTableHeader | Table.Header}
+   *
+   * This is the table header section. It is required!
+   */
+  Header: StyledTableHeader,
+
+  /**
+   * {@link StyledTableBody | Table.Body}
+   *
+   * This is the table body section. It is required!
+   */
+  Body: StyledTableBody,
+
+  /**
+   * {@link TableRow | Table.Row}
+   *
+   * This is a table row. Wrap your cells in it.
+   * You can make it selectable by defining the `selected` prop as well as adding a {@link TableCellCheckbox | Table.CellCheckbox}.
+   */
+  Row: TableRow,
+
+  /**
+   * {@link TableRowHeaderCell | Table.RowHeaderCell}
+   *
+   * This is a table row header cell. It must have a label or an aria-label.
+   * Each row should have a header cell to identify the item represented in that row.
+   * This will typically be a name, or identifier.
+   */
+  RowHeaderCell: TableRowHeaderCell,
+
+  /**
+   * {@link TableColumnHeaderCell | Table.ColumnHeaderCell}
+   *
+   * This is a table column header cell. It must have a label or an aria-label.
+   * It may have a toggletip to decribe its purpose.
+   *
+   * ** Sortable **
+   * Providing a `onSort` callback will turn this cells' contents into a button.
+   * The sorting mechanism must be implemented by you, but providing the sort order will
+   * automatically add an indicator and relevant aria attribute to the header.
+   *
+   */
+  ColumnHeaderCell: TableColumnHeaderCell,
+
+  /**
+   * {@link StyledTableCell | Table.Cell}
+   *
+   * This is a plain cell:
+   * it serves as an escape hatch if you need a cell with custom content
+   *
+   * Only use this as a last resort!
+   */
+  Cell: StyledTableCell,
+
+  /**
+   * {@link TableCellText | Table.CellText}
+   *
+   * This is a simple cell to display text
path: src/components/table/index.ts
user: login: david-cho-lerat-sonarsource
body: ```suggestion
   * This is a simple cell to display text.
```
created_at: 2025-05-26T16:04:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,39 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { forwardRef } from 'react';
+import { Badge, BadgeProps } from '../badges';
+import { StyledTableCell } from './TableStyles';
+
+export interface TableCellBadgesProps extends BadgeProps {
path: src/components/table/TableCellBadge.tsx
user: login: david-cho-lerat-sonarsource
body: shouldn't this be `TableCellBadgeProps`?
created_at: 2025-05-26T16:07:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,46 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import styled from '@emotion/styled';
+import { forwardRef } from 'react';
+import { ButtonIcon, ButtonIconProps } from '../buttons/ButtonIcon';
+import { StyledTableCell } from './TableStyles';
+
+export interface TableCellButtonIconProps extends ButtonIconProps {
+  cellClassName?: string;
+}
+
+export const TableCellButtonIcon = forwardRef<HTMLButtonElement, TableCellButtonIconProps>(
+  (props, ref) => {
+    const { cellClassName, ...buttonProps } = props;
+
+    return (
+      <StyledTableCellButtonIcon className={cellClassName}>
+        <ButtonIcon variety="default-ghost" {...buttonProps} ref={ref} />
path: src/components/table/TableCellButtonIcon.tsx
user: login: david-cho-lerat-sonarsource
body: NP: `ButtonVariety.DefaultGhost`?
created_at: 2025-05-26T16:09:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,52 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { forwardRef } from 'react';
+import { isDefined } from '~common/helpers/types';
+import { TextNode } from '~types/utils';
+import { LinkStandalone, LinkStandaloneProps } from '../links/LinkStandalone';
+import { Text } from '../typography';
+import { StyledContentWrapper, StyledTableCell } from './TableStyles';
+
+export interface TableCellLinkProps extends LinkStandaloneProps {
+  description?: TextNode;
+  cellClassName?: string;
path: src/components/table/TableCellLink.tsx
user: login: david-cho-lerat-sonarsource
body: NP: alpha order?
created_at: 2025-05-26T16:10:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,52 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { forwardRef } from 'react';
+import { isDefined } from '~common/helpers/types';
+import { TextNode } from '~types/utils';
+import { LinkStandalone, LinkStandaloneProps } from '../links/LinkStandalone';
+import { Text } from '../typography';
+import { StyledContentWrapper, StyledTableCell } from './TableStyles';
+
+export interface TableCellLinkProps extends LinkStandaloneProps {
+  description?: TextNode;
+  cellClassName?: string;
+}
+
+export const TableCellLink = forwardRef<HTMLAnchorElement, TableCellLinkProps>((props, ref) => {
+  const { cellClassName, description, children, ...linkProps } = props;
+
+  return (
+    <StyledTableCell className={cellClassName} css={{ justifyContent: 'left' }}>
+      <StyledContentWrapper>
+        <LinkStandalone {...linkProps} ref={ref}>
+          {children}
+        </LinkStandalone>
+        {isDefined(description) && (
path: src/components/table/TableCellLink.tsx
user: login: david-cho-lerat-sonarsource
body: ```suggestion

        {isDefined(description) && (
```
created_at: 2025-05-26T16:10:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,56 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { forwardRef } from 'react';
+import { isDefined } from '~common/helpers/types';
+import { TextNode } from '~types/utils';
+import { Text } from '../typography';
+import { StyledContentWrapper, StyledTableCell } from './TableStyles';
+
+export interface TableCellTextProps {
+  className?: string;
+  content: TextNode;
+  description?: TextNode;
+}
+
+export const TableCellNumber = forwardRef<HTMLTableCellElement, TableCellTextProps>(
+  (props, ref) => {
+    const { className, content, description, ...radixProps } = props;
+
+    return (
+      <StyledTableCell
+        className={className}
+        css={{ justifyContent: 'end' }}
+        ref={ref}
+        {...radixProps}>
+        <StyledContentWrapper css={{ alignItems: 'end' }}>
+          {content}
+          {isDefined(description) && (
path: src/components/table/TableCellNumber.tsx
user: login: david-cho-lerat-sonarsource
body: ```suggestion

          {isDefined(description) && (
```
created_at: 2025-05-26T16:11:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,56 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { forwardRef, ReactNode } from 'react';
+import { isDefined } from '~common/helpers/types';
+import { TextNode } from '~types/utils';
+import { Text } from '../typography';
+import { StyledContentWrapper, StyledTableCell } from './TableStyles';
+
+export interface TableCellTextProps {
+  className?: string;
+  content: TextNode;
+  description?: TextNode;
+  icon?: ReactNode;
+}
+
+export const TableCellText = forwardRef<HTMLTableCellElement, TableCellTextProps>((props, ref) => {
+  const { className, content, description, icon, ...radixProps } = props;
+
+  return (
+    <StyledTableCell
+      className={className}
+      css={{ justifyContent: 'start' }}
+      ref={ref}
+      {...radixProps}>
+      {icon}
+      <StyledContentWrapper>
path: src/components/table/TableCellText.tsx
user: login: david-cho-lerat-sonarsource
body: ```suggestion

      <StyledContentWrapper>
```
created_at: 2025-05-26T16:11:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,56 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { forwardRef, ReactNode } from 'react';
+import { isDefined } from '~common/helpers/types';
+import { TextNode } from '~types/utils';
+import { Text } from '../typography';
+import { StyledContentWrapper, StyledTableCell } from './TableStyles';
+
+export interface TableCellTextProps {
+  className?: string;
+  content: TextNode;
+  description?: TextNode;
+  icon?: ReactNode;
+}
+
+export const TableCellText = forwardRef<HTMLTableCellElement, TableCellTextProps>((props, ref) => {
+  const { className, content, description, icon, ...radixProps } = props;
+
+  return (
+    <StyledTableCell
+      className={className}
+      css={{ justifyContent: 'start' }}
+      ref={ref}
+      {...radixProps}>
+      {icon}
+      <StyledContentWrapper>
+        {content}
+        {isDefined(description) && (
path: src/components/table/TableCellText.tsx
user: login: david-cho-lerat-sonarsource
body: ```suggestion

        {isDefined(description) && (
```
created_at: 2025-05-26T16:11:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,137 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { screen } from '@testing-library/react';
+import { MemoryRouter, Route, Routes } from 'react-router-dom';
+import { render } from '~common/helpers/test-utils';
+import { Table } from '..';
+import { IconEdit } from '../../icons';
+
+it('should be accessible', () => {
+  setupTable();
+
+  expect(screen.getByRole('table')).toHaveNoA11yViolations({
+    // We accept non-visible labels
+    rules: { 'empty-table-header': { enabled: false } },
+  });
+});
+
+it('should allow selection', async () => {
+  const onCheckHeader = jest.fn();
+  const onCheckRow = jest.fn();
+  const { user } = setupTable({ onCheckRow, onCheckHeader });
+
+  expect(screen.getByRole('checkbox', { name: 'Select 1' })).toBeChecked();
+
+  await user.click(screen.getByRole('checkbox', { name: 'Select 2' }));
+
+  expect(onCheckRow).toHaveBeenCalledWith('2');
+
+  await user.click(screen.getByRole('checkbox', { name: 'Select all' }));
+
+  expect(onCheckHeader).toHaveBeenCalled();
+});
+
+it('should handle sorting', async () => {
+  const onSort = jest.fn();
+  const { user } = setupTable({ onSort });
+
+  expect(screen.getByRole('columnheader', { name: 'col 1' })).not.toHaveAttribute('aria-sort');
+  expect(screen.getByRole('columnheader', { name: 'col 2' })).toHaveAttribute(
+    'aria-sort',
+    'descending',
+  );
+
+  await user.click(screen.getByRole('button', { name: 'col 1' }));
+
+  expect(onSort).toHaveBeenCalledWith('1');
+});
+
+type RowType = [boolean, string, string, string];
+
+function setupTable(
+  args: {
+    onCheckHeader?: () => void;
+    onCheckRow?: () => void;
+    onSort?: (id: string) => void;
+    rows?: RowType[];
+  } = {},
+) {
+  const {
+    onCheckHeader = jest.fn(),
+    onCheckRow = jest.fn(),
+    onSort,
+    rows = [
+      [true, '1', 'first', 'cool'],
+      [false, '2', 'second', 'beans'],
+    ],
+  } = args;
+
+  return setupWithMemoryRouter(
+    <Table ariaLabel="awesome table" gridTemplate="max-content 1fr auto repeat(4, min-content)">
+      <Table.Header>
+        <Table.Row>
+          <Table.ColumnHeaderCellCheckbox
+            ariaLabel="Select all"
+            checked="indeterminate"
+            onCheck={onCheckHeader}
+          />
+          <Table.ColumnHeaderCell label="col 1" onSort={onSort && (() => onSort('1'))} />
+          <Table.ColumnHeaderCell
+            label="col 2"
+            onSort={onSort && (() => onSort('2'))}
+            sortDirection="desc"
+          />
+          <Table.ColumnHeaderCell label="Status" />
+          <Table.ColumnHeaderCell label="Related" />
+          <Table.ColumnHeaderCell label="Clickable" />
+          <Table.ColumnHeaderCell aria-label="Actions" />
+        </Table.Row>
+      </Table.Header>
+      <Table.Body>
path: src/components/table/__tests__/Table-test.tsx
user: login: david-cho-lerat-sonarsource
body: ```suggestion

      <Table.Body>
```
created_at: 2025-05-27T07:44:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,254 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import type { Meta, StoryObj } from '@storybook/react';
+import { useCallback, useState } from 'react';
+import { TableProps, TableSortDirection } from 'src/components/table/TableTypes';
+import {
+  BadgeVariety,
+  DropdownMenu,
+  IconMoreVertical,
+  IconWarning,
+  Popover,
+  Table,
+  TableVariety,
+  Tooltip,
+} from '../src';
+import { basicWrapperDecorator } from './helpers/BasicWrapper';
+
+const meta: Meta<typeof Table> = {
+  component: Table,
+  title: 'Echoes/Table',
+  parameters: {
+    controls: { exclude: ['children'] },
+  },
+  argTypes: {
+    variety: { control: { type: 'select' }, options: Object.values(TableVariety) },
+  },
+  decorators: [basicWrapperDecorator],
+};
+
+export default meta;
+
+type Story = StoryObj<typeof Table>;
+
+export const Selectable: Story = {
+  args: {
+    ariaLabel: 'Awesome table',
+    gridTemplate: 'max-content 1fr max-content 1fr 1fr max-content max-content',
+    variety: TableVariety.Surface,
+  },
+  render: (args) => <StateManager {...args} />,
+};
+
+const DATA = [
+  {
+    name: 'Michelangelo',
+    email: 'mikey@sewers.nyc',
+    weapon: 'Nunchaku',
+    age: '13',
+    badge: BadgeVariety.Warning,
+  },
+  {
+    name: 'Leonardo',
+    description: "He's the leader!",
+    email: 'leo@sewers.nyc',
+    weapon: 'Katana',
+    age: '15',
+    ageDescription: 'oldest',
+    badge: BadgeVariety.Info,
+  },
+  {
+    name: 'Donatello',
+    description: 'techie',
+    email: 'donnie@sewers.nyc',
+    weapon: 'Bo',
+    age: '14',
+    badge: BadgeVariety.Highlight,
+  },
+  {
+    name: 'Raphael',
+    email: 'raph@sewers.nyc',
+    emailDescription: "don't bother him!",
+    emailTooltip: 'he can be a bit impatient',
+    weapon: 'Sai',
+    age: '14',
+    badge: BadgeVariety.Danger,
+  },
+];
+
+function getSelectionState(selectedRows: Record<string, boolean>) {
+  const values = Object.values(selectedRows);
+
+  if (values.every((v) => v)) {
+    return true;
+  }
+
+  if (values.every((v) => !v)) {
+    return false;
+  }
+
+  return 'indeterminate';
+}
+
+function StateManager(props: TableProps) {
path: stories/Table-stories.tsx
user: login: david-cho-lerat-sonarsource
body: question: wouldn't it be useful to export at least part of this as "utils" in Echoes? People using Table will probably re-implement something similar?
created_at: 2025-05-27T07:55:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,128 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import styled from '@emotion/styled';
+import { forwardRef, ReactNode } from 'react';
+import { isDefined } from '~common/helpers/types';
+import { TextNode } from '~types/utils';
+import { Button } from '../buttons';
+import { IconArrowDown, IconArrowUp } from '../icons';
+import { ToggleTip, ToggleTipProps } from '../toggle-tip';
+import { StyledTableColumnHeaderCell } from './TableStyles';
+import { TableCellJustify, TableSortDirection } from './TableTypes';
+
+export interface TableColumnHeaderCellProps {
+  className?: string;
+  justify?: `${TableCellJustify}`;
+  label?: TextNode;
+  onSort?: () => void;
+  sortDirection?: `${TableSortDirection}`;
+  toggleTip?: ToggleTipProps;
+}
+
+function getSortProps(direction?: TableColumnHeaderCellProps['sortDirection']): {
+  aria: React.AriaAttributes['aria-sort'];
+  icon: ReactNode;
+} {
+  switch (direction) {
+    case TableSortDirection.Asc:
+      return {
+        aria: 'ascending',
+        icon: <IconArrowUp />,
+      };
+    case TableSortDirection.Desc:
path: src/components/table/TableColumnHeaderCell.tsx
user: login: david-cho-lerat-sonarsource
body: ```suggestion

    case TableSortDirection.Desc:
```
created_at: 2025-05-27T08:18:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,128 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import styled from '@emotion/styled';
+import { forwardRef, ReactNode } from 'react';
+import { isDefined } from '~common/helpers/types';
+import { TextNode } from '~types/utils';
+import { Button } from '../buttons';
+import { IconArrowDown, IconArrowUp } from '../icons';
+import { ToggleTip, ToggleTipProps } from '../toggle-tip';
+import { StyledTableColumnHeaderCell } from './TableStyles';
+import { TableCellJustify, TableSortDirection } from './TableTypes';
+
+export interface TableColumnHeaderCellProps {
+  className?: string;
+  justify?: `${TableCellJustify}`;
+  label?: TextNode;
+  onSort?: () => void;
+  sortDirection?: `${TableSortDirection}`;
+  toggleTip?: ToggleTipProps;
+}
+
+function getSortProps(direction?: TableColumnHeaderCellProps['sortDirection']): {
+  aria: React.AriaAttributes['aria-sort'];
+  icon: ReactNode;
+} {
+  switch (direction) {
+    case TableSortDirection.Asc:
+      return {
+        aria: 'ascending',
+        icon: <IconArrowUp />,
+      };
+    case TableSortDirection.Desc:
+      return {
+        aria: 'descending',
+        icon: <IconArrowDown />,
+      };
+    default:
path: src/components/table/TableColumnHeaderCell.tsx
user: login: david-cho-lerat-sonarsource
body: ```suggestion

    default:
```
created_at: 2025-05-27T08:18:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,128 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import styled from '@emotion/styled';
+import { forwardRef, ReactNode } from 'react';
+import { isDefined } from '~common/helpers/types';
+import { TextNode } from '~types/utils';
+import { Button } from '../buttons';
+import { IconArrowDown, IconArrowUp } from '../icons';
+import { ToggleTip, ToggleTipProps } from '../toggle-tip';
+import { StyledTableColumnHeaderCell } from './TableStyles';
+import { TableCellJustify, TableSortDirection } from './TableTypes';
+
+export interface TableColumnHeaderCellProps {
+  className?: string;
+  justify?: `${TableCellJustify}`;
+  label?: TextNode;
+  onSort?: () => void;
+  sortDirection?: `${TableSortDirection}`;
+  toggleTip?: ToggleTipProps;
+}
+
+function getSortProps(direction?: TableColumnHeaderCellProps['sortDirection']): {
+  aria: React.AriaAttributes['aria-sort'];
+  icon: ReactNode;
+} {
+  switch (direction) {
+    case TableSortDirection.Asc:
+      return {
+        aria: 'ascending',
+        icon: <IconArrowUp />,
+      };
+    case TableSortDirection.Desc:
+      return {
+        aria: 'descending',
+        icon: <IconArrowDown />,
+      };
+    default:
+      return {
+        aria: undefined,
+        icon: <StyledSortIconPlaceholder />,
+      };
+  }
+}
+
+export const TableColumnHeaderCell = forwardRef<HTMLTableCellElement, TableColumnHeaderCellProps>(
+  (props, ref) => {
+    const {
+      className,
+      justify = TableCellJustify.Start,
+      label,
+      onSort,
+      sortDirection,
+      toggleTip,
+      ...radixProps
+    } = props;
+
+    if (isDefined(onSort)) {
+      const sortProps = getSortProps(sortDirection);
+
+      return (
+        <StyledTableColumnHeaderCell
+          {...radixProps}
+          // ARIA sort is recommended by WCAG
+          // https://www.w3.org/WAI/ARIA/apg/practices/grid-and-table-properties/#indicatingsortorderwitharia-sort
+          aria-sort={sortProps.aria}
+          className={className}
+          css={{
+            justifyContent: justify,
+            padding: 'var(--echoes-dimension-space-100)',
+          }}
+          ref={ref}>
+          <Button onClick={onSort} size="medium" suffix={sortProps.icon} variety="default-ghost">
+            {label}
+          </Button>
+        </StyledTableColumnHeaderCell>
+      );
+    }
+
+    return (
+      <StyledTableColumnHeaderCell
+        {...radixProps}
+        className={className}
+        css={{ justifyContent: justify }}
+        ref={ref}>
+        {label}
+        {toggleTip && <StyledToggleTip {...toggleTip} />}
+      </StyledTableColumnHeaderCell>
+    );
+  },
+);
+
+TableColumnHeaderCell.displayName = 'TableColumnHeaderCell';
+
+/*
+ * This is a placeholder so the header label doesn't 'dance around'
+ * when sorting another column.
+ * It also appears on hover, to hint at the effect of clicking.
+ */
+const StyledSortIconPlaceholder = styled(IconArrowUp)`
+  visibility: hidden;
+
+  ${StyledTableColumnHeaderCell}:hover & {
+    visibility: visible;
+  }
+`;
+
+/* necessary to play well with 'min-content' */
path: src/components/table/TableColumnHeaderCell.tsx
user: login: david-cho-lerat-sonarsource
body: ```suggestion
/* Necessary to play well with 'min-content' */
```
created_at: 2025-05-27T08:19:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,254 @@
+/*
+ * Echoes React
+ * Copyright (C) 2023-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import type { Meta, StoryObj } from '@storybook/react';
+import { useCallback, useState } from 'react';
+import { TableProps, TableSortDirection } from 'src/components/table/TableTypes';
+import {
+  BadgeVariety,
+  DropdownMenu,
+  IconMoreVertical,
+  IconWarning,
+  Popover,
+  Table,
+  TableVariety,
+  Tooltip,
+} from '../src';
+import { basicWrapperDecorator } from './helpers/BasicWrapper';
+
+const meta: Meta<typeof Table> = {
+  component: Table,
+  title: 'Echoes/Table',
+  parameters: {
+    controls: { exclude: ['children'] },
+  },
+  argTypes: {
+    variety: { control: { type: 'select' }, options: Object.values(TableVariety) },
+  },
+  decorators: [basicWrapperDecorator],
+};
+
+export default meta;
+
+type Story = StoryObj<typeof Table>;
+
+export const Selectable: Story = {
+  args: {
+    ariaLabel: 'Awesome table',
+    gridTemplate: 'max-content 1fr max-content 1fr 1fr max-content max-content',
+    variety: TableVariety.Surface,
+  },
+  render: (args) => <StateManager {...args} />,
+};
+
+const DATA = [
+  {
+    name: 'Michelangelo',
+    email: 'mikey@sewers.nyc',
+    weapon: 'Nunchaku',
+    age: '13',
+    badge: BadgeVariety.Warning,
+  },
+  {
+    name: 'Leonardo',
+    description: "He's the leader!",
+    email: 'leo@sewers.nyc',
+    weapon: 'Katana',
+    age: '15',
+    ageDescription: 'oldest',
+    badge: BadgeVariety.Info,
+  },
+  {
+    name: 'Donatello',
+    description: 'techie',
+    email: 'donnie@sewers.nyc',
+    weapon: 'Bo',
+    age: '14',
+    badge: BadgeVariety.Highlight,
+  },
+  {
+    name: 'Raphael',
+    email: 'raph@sewers.nyc',
+    emailDescription: "don't bother him!",
+    emailTooltip: 'he can be a bit impatient',
+    weapon: 'Sai',
+    age: '14',
+    badge: BadgeVariety.Danger,
+  },
+];
+
+function getSelectionState(selectedRows: Record<string, boolean>) {
+  const values = Object.values(selectedRows);
+
+  if (values.every((v) => v)) {
+    return true;
+  }
+
+  if (values.every((v) => !v)) {
+    return false;
+  }
+
+  return 'indeterminate';
+}
+
+function StateManager(props: TableProps) {
path: stories/Table-stories.tsx
user: login: jeremy-davis-sonarsource
body: I considered it, but I think we're better off creating a hook in the shared module
created_at: 2025-05-27T14:09:29Z
author_association: CONTRIBUTOR

PR comments:
user: login: netlify[bot]
created_at: 2025-05-23T07:30:51Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *echoes-react* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 9bbbeca33b3b9c873851550ed3e80d3e53ece18c |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/projects/echoes-react/deploys/6835c878bb296800083d31f5 |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-333--echoes-react.netlify.app](https://deploy-preview-333--echoes-react.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTMzMy0tZWNob2VzLXJlYWN0Lm5ldGxpZnkuYXBwIn0.-y-3miI8tULzh0CKc7vfRdLlBenwPKbLnCO-UK3Sms0)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [echoes-react Preview](https://deploy-preview-333--echoes-react.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify project configuration](https://app.netlify.com/projects/echoes-react/configuration/notifications#deploy-notifications)._
author_association: NONE
user: login: sonarqube-next[bot]
created_at: 2025-05-27T14:15:36Z
body: ## [![Quality Gate passed](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://next.sonarqube.com/sonarqube/dashboard?id=SonarSource_echoes-react_AYvOIyNg-JQvdKIPB6Ig&pullRequest=333) **Quality Gate passed**  
Issues  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://next.sonarqube.com/sonarqube/project/issues?id=SonarSource_echoes-react_AYvOIyNg-JQvdKIPB6Ig&pullRequest=333&resolved=false) [0 New issues](https://next.sonarqube.com/sonarqube/project/issues?id=SonarSource_echoes-react_AYvOIyNg-JQvdKIPB6Ig&pullRequest=333&resolved=false)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/fixed-16px.png '')](https://next.sonarqube.com/sonarqube/project/issues?id=SonarSource_echoes-react_AYvOIyNg-JQvdKIPB6Ig&fixedInPullRequest=333) [0 Fixed issues](https://next.sonarqube.com/sonarqube/project/issues?id=SonarSource_echoes-react_AYvOIyNg-JQvdKIPB6Ig&fixedInPullRequest=333)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/accepted-16px.png '')](https://next.sonarqube.com/sonarqube/project/issues?id=SonarSource_echoes-react_AYvOIyNg-JQvdKIPB6Ig&pullRequest=333&issueStatuses=ACCEPTED) [0 Accepted issues](https://next.sonarqube.com/sonarqube/project/issues?id=SonarSource_echoes-react_AYvOIyNg-JQvdKIPB6Ig&pullRequest=333&issueStatuses=ACCEPTED)

Measures  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://next.sonarqube.com/sonarqube/security_hotspots?id=SonarSource_echoes-react_AYvOIyNg-JQvdKIPB6Ig&pullRequest=333) [0 Security Hotspots](https://next.sonarqube.com/sonarqube/security_hotspots?id=SonarSource_echoes-react_AYvOIyNg-JQvdKIPB6Ig&pullRequest=333)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://next.sonarqube.com/sonarqube/dependency-risks?id=SonarSource_echoes-react_AYvOIyNg-JQvdKIPB6Ig&pullRequest=333) [0 Dependency risks](https://next.sonarqube.com/sonarqube/dependency-risks?id=SonarSource_echoes-react_AYvOIyNg-JQvdKIPB6Ig&pullRequest=333)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://next.sonarqube.com/sonarqube/component_measures?id=SonarSource_echoes-react_AYvOIyNg-JQvdKIPB6Ig&pullRequest=333&metric=new_coverage&view=list) [92.3% Coverage on New Code](https://next.sonarqube.com/sonarqube/component_measures?id=SonarSource_echoes-react_AYvOIyNg-JQvdKIPB6Ig&pullRequest=333&metric=new_coverage&view=list)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://next.sonarqube.com/sonarqube/component_measures?id=SonarSource_echoes-react_AYvOIyNg-JQvdKIPB6Ig&pullRequest=333&metric=new_duplicated_lines_density&view=list) [0.0% Duplication on New Code](https://next.sonarqube.com/sonarqube/component_measures?id=SonarSource_echoes-react_AYvOIyNg-JQvdKIPB6Ig&pullRequest=333&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube](https://next.sonarqube.com/sonarqube/dashboard?id=SonarSource_echoes-react_AYvOIyNg-JQvdKIPB6Ig&pullRequest=333)


author_association: NONE