diff --git a/client/src/components/Chat/Messages/Content/Markdown.tsx b/client/src/components/Chat/Messages/Content/Markdown.tsx
index 1217869a2c..4daef8f80b 100644
--- a/client/src/components/Chat/Messages/Content/Markdown.tsx
+++ b/client/src/components/Chat/Messages/Content/Markdown.tsx
@@ -19,7 +19,7 @@ import { ArtifactProvider, CodeBlockProvider } from '~/Providers';
import MarkdownErrorBoundary from './MarkdownErrorBoundary';
import { langSubset, preprocessLaTeX } from '~/utils';
import { unicodeCitation } from '~/components/Web';
-import { code, a, p, img } from './MarkdownComponents';
+import { code, a, p, img, table } from './MarkdownComponents';
import store from '~/store';
type TContentProps = {
@@ -88,6 +88,7 @@ const Markdown = memo(function Markdown({ content = '', isLatestMessage }: TCont
a,
p,
img,
+ table,
artifact: Artifact,
citation: Citation,
'highlighted-text': HighlightedText,
diff --git a/client/src/components/Chat/Messages/Content/MarkdownComponents.tsx b/client/src/components/Chat/Messages/Content/MarkdownComponents.tsx
index c38c40f1e1..5147dd2a9a 100644
--- a/client/src/components/Chat/Messages/Content/MarkdownComponents.tsx
+++ b/client/src/components/Chat/Messages/Content/MarkdownComponents.tsx
@@ -185,6 +185,19 @@ export const p: React.ElementType = memo(function MarkdownParagraph({ children }
});
p.displayName = 'MarkdownParagraph';
+type TTableProps = {
+ children: React.ReactNode;
+};
+
+export const table: React.ElementType = memo(function MarkdownTable({ children }: TTableProps) {
+ return (
+
+ );
+});
+table.displayName = 'MarkdownTable';
+
type TImageProps = {
src?: string;
alt?: string;
diff --git a/client/src/components/Chat/Messages/Content/MarkdownErrorBoundary.tsx b/client/src/components/Chat/Messages/Content/MarkdownErrorBoundary.tsx
index 0342c60f8a..9862da2102 100644
--- a/client/src/components/Chat/Messages/Content/MarkdownErrorBoundary.tsx
+++ b/client/src/components/Chat/Messages/Content/MarkdownErrorBoundary.tsx
@@ -4,7 +4,7 @@ import supersub from 'remark-supersub';
import ReactMarkdown from 'react-markdown';
import rehypeHighlight from 'rehype-highlight';
import type { PluggableList } from 'unified';
-import { code, codeNoExecution, a, p } from './MarkdownComponents';
+import { code, codeNoExecution, a, p, table } from './MarkdownComponents';
import { CodeBlockProvider } from '~/Providers';
import { langSubset } from '~/utils';
@@ -72,6 +72,7 @@ class MarkdownErrorBoundary extends React.Component<
code: codeExecution ? code : codeNoExecution,
a,
p,
+ table,
} as {
[nodeType: string]: React.ElementType;
}
diff --git a/client/src/components/Chat/Messages/Content/MarkdownLite.tsx b/client/src/components/Chat/Messages/Content/MarkdownLite.tsx
index 24980d8a90..44656ae77f 100644
--- a/client/src/components/Chat/Messages/Content/MarkdownLite.tsx
+++ b/client/src/components/Chat/Messages/Content/MarkdownLite.tsx
@@ -6,7 +6,7 @@ import supersub from 'remark-supersub';
import ReactMarkdown from 'react-markdown';
import rehypeHighlight from 'rehype-highlight';
import type { PluggableList } from 'unified';
-import { code, codeNoExecution, a, p, img } from './MarkdownComponents';
+import { code, codeNoExecution, a, p, img, table } from './MarkdownComponents';
import { CodeBlockProvider, ArtifactProvider } from '~/Providers';
import MarkdownErrorBoundary from './MarkdownErrorBoundary';
import { langSubset } from '~/utils';
@@ -44,6 +44,7 @@ const MarkdownLite = memo(
a,
p,
img,
+ table,
} as {
[nodeType: string]: React.ElementType;
}
diff --git a/client/src/components/Chat/Messages/Content/__tests__/Markdown.mcpui.test.tsx b/client/src/components/Chat/Messages/Content/__tests__/Markdown.mcpui.test.tsx
index 6ca06056fa..bde9614904 100644
--- a/client/src/components/Chat/Messages/Content/__tests__/Markdown.mcpui.test.tsx
+++ b/client/src/components/Chat/Messages/Content/__tests__/Markdown.mcpui.test.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import Markdown from '../Markdown';
+import MarkdownLite from '../MarkdownLite';
import { RecoilRoot } from 'recoil';
import { UI_RESOURCE_MARKER } from '~/components/MCPUIResource/plugin';
import {
@@ -108,3 +109,35 @@ describe('Markdown with MCP UI markers (resource IDs)', () => {
expect(renderers[1]).toHaveAttribute('data-resource-uri', 'ui://weather/nyc');
});
});
+
+describe('Markdown table rendering', () => {
+ const tableMarkdown = [
+ '| Alpha | Bravo | Charlie | Delta | Echo | Foxtrot | Golf | Hotel |',
+ '| --- | --- | --- | --- | --- | --- | --- | --- |',
+ '| one | two | three | four | five | six | seven | eight |',
+ ].join('\n');
+
+ it('wraps GFM tables in a horizontally scrollable container', () => {
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByRole('table').parentElement).toHaveClass(
+ 'markdown-table-wrapper',
+ 'w-full',
+ 'max-w-full',
+ );
+ });
+
+ it('wraps lightweight Markdown tables in a horizontally scrollable container', () => {
+ render();
+
+ expect(screen.getByRole('table').parentElement).toHaveClass(
+ 'markdown-table-wrapper',
+ 'w-full',
+ 'max-w-full',
+ );
+ });
+});
diff --git a/client/src/style.css b/client/src/style.css
index 902860ef52..35dee56a1d 100644
--- a/client/src/style.css
+++ b/client/src/style.css
@@ -1496,6 +1496,27 @@ button {
background-color: transparent;
}
+.markdown-table-wrapper {
+ -webkit-overflow-scrolling: touch;
+ overflow-x: auto;
+ overscroll-behavior-x: contain;
+ padding-bottom: 0.25rem;
+ scrollbar-color: var(--border-medium) transparent;
+ scrollbar-width: thin;
+}
+
+.markdown-table-wrapper::-webkit-scrollbar {
+ height: 0.5rem;
+}
+
+.markdown-table-wrapper::-webkit-scrollbar-thumb {
+ background-color: var(--border-medium);
+}
+
+.markdown-table-wrapper::-webkit-scrollbar-thumb:hover {
+ background-color: var(--border-heavy);
+}
+
/* Show scrollbar only on hover */
.scrollbar-hover {
scrollbar-width: thin;