diff --git a/e2e/test/scenarios/metrics/browse.cy.spec.ts b/e2e/test/scenarios/metrics/browse.cy.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c314987de52164d9f000507e2b9fa12a87738323
--- /dev/null
+++ b/e2e/test/scenarios/metrics/browse.cy.spec.ts
@@ -0,0 +1,233 @@
+import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
+import {
+  FIRST_COLLECTION_ID,
+  ORDERS_MODEL_ID,
+} from "e2e/support/cypress_sample_instance_data";
+import {
+  type StructuredQuestionDetails,
+  assertIsEllipsified,
+  createQuestion,
+  main,
+  navigationSidebar,
+  restore,
+} from "e2e/support/helpers";
+
+const { ORDERS_ID, ORDERS, PRODUCTS_ID } = SAMPLE_DATABASE;
+
+type StructuredQuestionDetailsWithName = StructuredQuestionDetails & {
+  name: string;
+};
+
+const ORDERS_SCALAR_METRIC: StructuredQuestionDetailsWithName = {
+  name: "Count of orders",
+  type: "metric",
+  description: "A metric",
+  query: {
+    "source-table": ORDERS_ID,
+    aggregation: [["count"]],
+  },
+  display: "scalar",
+};
+
+const ORDERS_SCALAR_MODEL_METRIC: StructuredQuestionDetailsWithName = {
+  name: "Orders model metric",
+  type: "metric",
+  description: "A metric",
+  query: {
+    "source-table": `card__${ORDERS_MODEL_ID}`,
+    aggregation: [["count"]],
+  },
+  display: "scalar",
+  collection_id: FIRST_COLLECTION_ID as number,
+};
+
+const ORDERS_TIMESERIES_METRIC: StructuredQuestionDetailsWithName = {
+  name: "Count of orders over time",
+  type: "metric",
+  description: "A metric",
+  query: {
+    "source-table": ORDERS_ID,
+    aggregation: [["count"]],
+    breakout: [
+      [
+        "field",
+        ORDERS.CREATED_AT,
+        { "base-type": "type/DateTime", "temporal-unit": "month" },
+      ],
+    ],
+  },
+  display: "line",
+};
+
+const PRODUCTS_SCALAR_METRIC: StructuredQuestionDetailsWithName = {
+  name: "Count of products",
+  type: "metric",
+  description: "A metric",
+  query: {
+    "source-table": PRODUCTS_ID,
+    aggregation: [["count"]],
+  },
+  display: "scalar",
+};
+
+const ALL_METRICS = [
+  ORDERS_SCALAR_METRIC,
+  ORDERS_SCALAR_MODEL_METRIC,
+  ORDERS_TIMESERIES_METRIC,
+  PRODUCTS_SCALAR_METRIC,
+];
+
+function createMetrics(
+  metrics: StructuredQuestionDetailsWithName[] = ALL_METRICS,
+) {
+  metrics.forEach(metric => createQuestion(metric));
+}
+
+function metricsTable() {
+  return cy.findByLabelText("Table of metrics").should("be.visible");
+}
+
+function findMetric(name: string) {
+  return metricsTable().findByText(name).should("be.visible");
+}
+
+function getMetricsTableItem(index: number) {
+  return metricsTable().findAllByTestId("metric-name").eq(index);
+}
+
+const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+
+describe("scenarios > browse > metrics", () => {
+  beforeEach(() => {
+    restore();
+    cy.signInAsNormalUser();
+    cy.intercept("POST", "/api/dataset").as("dataset");
+  });
+
+  describe("no metrics", () => {
+    it("should hide the browse metrics link in the sidebar", () => {
+      cy.visit("/");
+      navigationSidebar().findByText("Metrics").should("not.exist");
+    });
+
+    it("should show the empty metrics page", () => {
+      cy.visit("/browse/metrics");
+      main()
+        .findByText(
+          "Metrics help you summarize and analyze your data effortlessly.",
+        )
+        .should("be.visible");
+    });
+  });
+
+  describe("multiple metrics", () => {
+    it("can browse metrics", () => {
+      createMetrics(ALL_METRICS);
+      cy.visit("/browse/metrics");
+      navigationSidebar().findByText("Metrics").should("be.visible");
+
+      ALL_METRICS.forEach(metric => {
+        findMetric(metric.name).should("be.visible");
+      });
+    });
+
+    it("should navigate to the metric when clicking a metric title", () => {
+      createMetrics([ORDERS_SCALAR_METRIC]);
+      cy.visit("/browse/metrics");
+      findMetric(ORDERS_SCALAR_METRIC.name).should("be.visible").click();
+      cy.location("pathname").should("match", /^\/metric\/\d+-.*$/);
+    });
+
+    it("should navigate to that collection when clicking a collection title", () => {
+      createMetrics([ORDERS_SCALAR_METRIC]);
+      cy.visit("/browse/metrics");
+      findMetric(ORDERS_SCALAR_METRIC.name).should("be.visible");
+
+      metricsTable().findByText("Our analytics").should("be.visible").click();
+
+      cy.location("pathname").should("eq", "/collection/root");
+    });
+
+    it("should open the collections in a new tab when alt-clicking a metric", () => {
+      cy.on("window:before:load", win => {
+        // prevent Cypress opening in a new window/tab and spy on this method
+        cy.stub(win, "open").as("open");
+      });
+
+      createMetrics([ORDERS_SCALAR_METRIC]);
+      cy.visit("/browse/metrics");
+
+      const macOSX = Cypress.platform === "darwin";
+      findMetric(ORDERS_SCALAR_METRIC.name).should("be.visible").click({
+        metaKey: macOSX,
+        ctrlKey: !macOSX,
+      });
+
+      cy.get("@open").should("have.been.calledOnce");
+      cy.get("@open").should(
+        "have.been.calledWithMatch",
+        /^\/question\/\d+-.*$/,
+        "_blank",
+      );
+
+      // the page did not navigate on this page
+      cy.location("pathname").should("eq", "/browse/metrics");
+    });
+
+    it("should render truncated markdown in the table", () => {
+      const description =
+        "This is a _very_ **long description** that should be truncated";
+
+      createMetrics([
+        {
+          ...ORDERS_SCALAR_METRIC,
+          description,
+        },
+      ]);
+
+      cy.visit("/browse/metrics");
+
+      metricsTable()
+        .findByText(/This is a/)
+        .should("be.visible")
+        .then(el => assertIsEllipsified(el[0]));
+
+      metricsTable()
+        .findByText(/This is a/)
+        .realHover();
+
+      cy.findAllByText(/should be truncated/).should("have.length", 2);
+    });
+
+    it("should be possible to sort the metrics", () => {
+      createMetrics(
+        ALL_METRICS.map((metric, index) => ({
+          ...metric,
+          name: `Metric ${alphabet[index]}`,
+          description: `Description ${alphabet[25 - index]}`,
+        })),
+      );
+
+      cy.visit("/browse/metrics");
+
+      getMetricsTableItem(0).should("contain", "Metric A");
+      getMetricsTableItem(1).should("contain", "Metric B");
+      getMetricsTableItem(2).should("contain", "Metric C");
+      getMetricsTableItem(3).should("contain", "Metric D");
+
+      metricsTable().findByText("Description").click();
+
+      getMetricsTableItem(0).should("contain", "Metric D");
+      getMetricsTableItem(1).should("contain", "Metric C");
+      getMetricsTableItem(2).should("contain", "Metric B");
+      getMetricsTableItem(3).should("contain", "Metric A");
+
+      metricsTable().findByText("Collection").click();
+
+      getMetricsTableItem(0).should("contain", "Metric B");
+      getMetricsTableItem(1).should("contain", "Metric A");
+      getMetricsTableItem(2).should("contain", "Metric C");
+      getMetricsTableItem(3).should("contain", "Metric D");
+    });
+  });
+});
diff --git a/e2e/test/scenarios/onboarding/home/browse.cy.spec.ts b/e2e/test/scenarios/onboarding/home/browse.cy.spec.ts
index fbc476ed4acf15a474fbb604d33150fff84054d0..3f0be43433c3505991b190710454d78fe9c9f371 100644
--- a/e2e/test/scenarios/onboarding/home/browse.cy.spec.ts
+++ b/e2e/test/scenarios/onboarding/home/browse.cy.spec.ts
@@ -36,6 +36,26 @@ describeWithSnowplow("scenarios > browse", () => {
     });
   });
 
+  it("can browse to a model in a new tab by meta-clicking", () => {
+    cy.on("window:before:load", win => {
+      // prevent Cypress opening in a new window/tab and spy on this method
+      cy.stub(win, "open").as("open");
+    });
+    cy.visit("/browse/models");
+    const macOSX = Cypress.platform === "darwin";
+    cy.findByRole("heading", { name: "Orders Model" }).click({
+      metaKey: macOSX,
+      ctrlKey: !macOSX,
+    });
+
+    cy.get("@open").should("have.been.calledOnce");
+    cy.get("@open").should(
+      "have.been.calledOnceWithExactly",
+      `/question/${ORDERS_MODEL_ID}-orders-model`,
+      "_blank",
+    );
+  });
+
   it("can browse to a table in a database", () => {
     cy.visit("/");
     browseDatabases().click();
diff --git a/frontend/src/metabase/browse/components/BrowseMetrics.tsx b/frontend/src/metabase/browse/components/BrowseMetrics.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..9a3a4157bf6a7f36f481e48fd6ab126be2daa4a1
--- /dev/null
+++ b/frontend/src/metabase/browse/components/BrowseMetrics.tsx
@@ -0,0 +1,90 @@
+import { t } from "ttag";
+
+import NoResults from "assets/img/metrics_bot.svg";
+import { useFetchMetrics } from "metabase/common/hooks/use-fetch-metrics";
+import EmptyState from "metabase/components/EmptyState";
+import { DelayedLoadingAndErrorWrapper } from "metabase/components/LoadingAndErrorWrapper/DelayedLoadingAndErrorWrapper";
+import { Box, Flex, Group, Icon, Stack, Text, Title } from "metabase/ui";
+
+import type { MetricResult } from "../types";
+
+import {
+  BrowseContainer,
+  BrowseHeader,
+  BrowseMain,
+  BrowseSection,
+} from "./BrowseContainer.styled";
+import { MetricsTable } from "./MetricsTable";
+
+export function BrowseMetrics() {
+  const metricsResult = useFetchMetrics({
+    filter_items_in_personal_collection: "exclude",
+    model_ancestors: false,
+  });
+  const metrics = metricsResult.data?.data as MetricResult[] | undefined;
+
+  const isEmpty = !metricsResult.isLoading && !metrics?.length;
+
+  return (
+    <BrowseContainer>
+      <BrowseHeader role="heading" data-testid="browse-metrics-header">
+        <BrowseSection>
+          <Flex
+            w="100%"
+            h="2.25rem"
+            direction="row"
+            justify="space-between"
+            align="center"
+          >
+            <Title order={1} color="text-dark">
+              <Group spacing="sm">
+                <Icon
+                  size={24}
+                  color="var(--mb-color-icon-primary)"
+                  name="metric"
+                />
+                {t`Metrics`}
+              </Group>
+            </Title>
+          </Flex>
+        </BrowseSection>
+      </BrowseHeader>
+      <BrowseMain>
+        <BrowseSection>
+          <Stack mb="lg" spacing="md" w="100%">
+            {isEmpty ? (
+              <MetricsEmptyState />
+            ) : (
+              <DelayedLoadingAndErrorWrapper
+                error={metricsResult.error}
+                loading={metricsResult.isLoading}
+                style={{ flex: 1 }}
+                loader={<MetricsTable skeleton />}
+              >
+                <MetricsTable metrics={metrics} />
+              </DelayedLoadingAndErrorWrapper>
+            )}
+          </Stack>
+        </BrowseSection>
+      </BrowseMain>
+    </BrowseContainer>
+  );
+}
+
+function MetricsEmptyState() {
+  return (
+    <Flex align="center" justify="center" mih="70vh">
+      <Box maw="25rem">
+        <EmptyState
+          title={t`Metrics help you summarize and analyze your data effortlessly.`}
+          message={
+            <Text mt="sm" maw="25rem">
+              {t`Metrics are like pre-defined calculations: create your aggregations once, save them as metrics, and use them whenever you need to analyze your data.`}
+            </Text>
+          }
+          illustrationElement={<img src={NoResults} />}
+        />
+      </Box>
+    </Flex>
+  );
+}
diff --git a/frontend/src/metabase/browse/components/BrowseMetrics.unit.spec.tsx b/frontend/src/metabase/browse/components/BrowseMetrics.unit.spec.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..0941d4c431828e98c588efe99e575f1b22efdbf5
--- /dev/null
+++ b/frontend/src/metabase/browse/components/BrowseMetrics.unit.spec.tsx
@@ -0,0 +1,255 @@
+import {
+  setupRecentViewsEndpoints,
+  setupSearchEndpoints,
+  setupSettingsEndpoints,
+} from "__support__/server-mocks";
+import { renderWithProviders, screen, within } from "__support__/ui";
+import {
+  createMockCollection,
+  createMockSearchResult,
+} from "metabase-types/api/mocks";
+import { createMockSetupState } from "metabase-types/store/mocks";
+
+import { createMockMetricResult, createMockRecentMetric } from "../test-utils";
+import type { MetricResult, RecentMetric } from "../types";
+
+import { BrowseMetrics } from "./BrowseMetrics";
+
+type SetupOpts = {
+  metricCount?: number;
+  recentMetricCount?: number;
+};
+
+function setup({
+  metricCount = Infinity,
+  recentMetricCount = 5,
+}: SetupOpts = {}) {
+  const mockMetricResults = mockMetrics.map(createMockMetricResult);
+  const mockRecentMetrics = mockMetrics.map(metric =>
+    createMockRecentMetric(metric as RecentMetric),
+  );
+
+  const metrics = mockMetricResults.slice(0, metricCount);
+  const recentMetrics = mockRecentMetrics.slice(0, recentMetricCount);
+
+  setupSettingsEndpoints([]);
+  setupSearchEndpoints(metrics.map(createMockSearchResult));
+  setupRecentViewsEndpoints(recentMetrics);
+
+  return renderWithProviders(<BrowseMetrics />, {
+    storeInitialState: {
+      setup: createMockSetupState({
+        locale: { name: "English", code: "en" },
+      }),
+    },
+  });
+}
+
+const defaultRootCollection = createMockCollection({
+  id: "root",
+  name: "Our analytics",
+});
+
+const collectionAlpha = createMockCollection({ id: 99, name: "Alpha" });
+const collectionBeta = createMockCollection({
+  id: 1,
+  name: "Beta",
+  effective_ancestors: [collectionAlpha],
+});
+const collectionCharlie = createMockCollection({
+  id: 2,
+  name: "Charlie",
+  effective_ancestors: [collectionAlpha, collectionBeta],
+});
+const collectionDelta = createMockCollection({
+  id: 3,
+  name: "Delta",
+  effective_ancestors: [collectionAlpha, collectionBeta, collectionCharlie],
+});
+const collectionZulu = createMockCollection({
+  id: 4,
+  name: "Zulu",
+  effective_ancestors: [
+    collectionAlpha,
+    collectionBeta,
+    collectionCharlie,
+    collectionDelta,
+  ],
+});
+const collectionAngstrom = createMockCollection({
+  id: 5,
+  name: "Ångström",
+  effective_ancestors: [
+    collectionAlpha,
+    collectionBeta,
+    collectionCharlie,
+    collectionDelta,
+    collectionZulu,
+  ],
+});
+const collectionOzgur = createMockCollection({
+  id: 6,
+  name: "Özgür",
+  effective_ancestors: [
+    collectionAlpha,
+    collectionBeta,
+    collectionCharlie,
+    collectionDelta,
+    collectionZulu,
+    collectionAngstrom,
+  ],
+});
+const collectionGrande = createMockCollection({
+  id: 7,
+  name: "Grande",
+  effective_ancestors: [
+    collectionAlpha,
+    collectionBeta,
+    collectionCharlie,
+    collectionDelta,
+    collectionZulu,
+    collectionAngstrom,
+    collectionOzgur,
+  ],
+});
+
+const mockMetrics: Partial<MetricResult>[] = [
+  {
+    id: 0,
+    collection: collectionAlpha,
+  },
+  {
+    id: 1,
+    collection: collectionAlpha,
+  },
+  {
+    id: 2,
+    collection: collectionAlpha,
+  },
+  {
+    id: 3,
+    collection: collectionBeta,
+  },
+  {
+    id: 4,
+    collection: collectionBeta,
+  },
+  {
+    id: 5,
+    collection: collectionBeta,
+  },
+  {
+    id: 6,
+    collection: collectionCharlie,
+  },
+  {
+    id: 7,
+    collection: collectionCharlie,
+  },
+  {
+    id: 8,
+    collection: collectionCharlie,
+  },
+  {
+    id: 9,
+    collection: collectionDelta,
+  },
+  {
+    id: 10,
+    collection: collectionDelta,
+  },
+  {
+    id: 11,
+    collection: collectionDelta,
+  },
+  {
+    id: 12,
+    collection: collectionZulu,
+  },
+  {
+    id: 13,
+    collection: collectionZulu,
+  },
+  {
+    id: 14,
+    collection: collectionZulu,
+  },
+  {
+    id: 15,
+    collection: collectionAngstrom,
+  },
+  {
+    id: 16,
+    collection: collectionAngstrom,
+  },
+  {
+    id: 17,
+    collection: collectionAngstrom,
+  },
+  {
+    id: 18,
+    collection: collectionOzgur,
+  },
+  {
+    id: 19,
+    collection: collectionOzgur,
+  },
+  {
+    id: 20,
+    collection: collectionOzgur,
+  },
+  {
+    id: 21,
+    collection: defaultRootCollection, // Our analytics
+  },
+  {
+    id: 22,
+    collection: defaultRootCollection, // Our analytics
+  },
+  ...new Array(100).fill(null).map((_, i) => ({
+    id: i + 300,
+    collection: collectionGrande,
+  })),
+].map((partialMetric: Partial<MetricResult>) => ({
+  name: `Metric ${partialMetric.id}`,
+  collection: defaultRootCollection,
+  last_editor_common_name: "Bobby",
+  last_edited_at: "2000-01-01T00:00:00.000Z",
+  ...partialMetric,
+}));
+
+describe("BrowseMetrics", () => {
+  it("displays an empty message when no metrics are found", async () => {
+    setup({ metricCount: 0 });
+    expect(
+      await screen.findByText(
+        "Metrics help you summarize and analyze your data effortlessly.",
+      ),
+    ).toBeInTheDocument();
+  });
+
+  it("displays the Our Analytics collection if it has a metric", async () => {
+    setup({ metricCount: 25 });
+    const table = await screen.findByRole("table", {
+      name: /Table of metrics/,
+    });
+    expect(table).toBeInTheDocument();
+    expect(
+      within(table).getAllByTestId("path-for-collection: Our analytics"),
+    ).toHaveLength(2);
+    expect(within(table).getByText("Metric 20")).toBeInTheDocument();
+    expect(within(table).getByText("Metric 21")).toBeInTheDocument();
+    expect(within(table).getByText("Metric 22")).toBeInTheDocument();
+  });
+
+  it("displays collection breadcrumbs", async () => {
+    setup({ metricCount: 5 });
+    const table = await screen.findByRole("table", {
+      name: /Table of metrics/,
+    });
+    expect(within(table).getByText("Metric 1")).toBeInTheDocument();
+    expect(
+      within(table).getAllByTestId("path-for-collection: Alpha"),
+    ).toHaveLength(3);
+  });
+});
diff --git a/frontend/src/metabase/browse/components/BrowseModels.tsx b/frontend/src/metabase/browse/components/BrowseModels.tsx
index 49acf1d69c139744685927c45c37cb8635d7bccf..94e62e4c21eec54549dce340d37c35b8d9d65563 100644
--- a/frontend/src/metabase/browse/components/BrowseModels.tsx
+++ b/frontend/src/metabase/browse/components/BrowseModels.tsx
@@ -95,7 +95,11 @@ export const BrowseModels = () => {
           >
             <Title order={1} color="text-dark">
               <Group spacing="sm">
-                <Icon size={24} color={"var(--mb-color-brand)"} name="model" />
+                <Icon
+                  size={24}
+                  color="var(--mb-color-icon-primary)"
+                  name="model"
+                />
                 {t`Models`}
               </Group>
             </Title>
@@ -135,7 +139,6 @@ export const BrowseModels = () => {
                     recentModelsResult.isLoading || modelsResult.isLoading
                   }
                   style={{ flex: 1 }}
-                  delay={0}
                   loader={<RecentModels skeleton />}
                 >
                   <RecentModels models={recentModels} />
@@ -144,7 +147,6 @@ export const BrowseModels = () => {
                   error={modelsResult.error}
                   loading={modelsResult.isLoading}
                   style={{ flex: 1 }}
-                  delay={0}
                   loader={<ModelsTable skeleton />}
                 >
                   <ModelsTable models={filteredModels} />
diff --git a/frontend/src/metabase/browse/components/ModelsTable.styled.tsx b/frontend/src/metabase/browse/components/BrowseTable.styled.tsx
similarity index 51%
rename from frontend/src/metabase/browse/components/ModelsTable.styled.tsx
rename to frontend/src/metabase/browse/components/BrowseTable.styled.tsx
index 2a6f451d48761b58d09e5d2c2fa2cfd9f9f0c154..01fa856e0e24c385f2a2f7fcfb55beb690d501e8 100644
--- a/frontend/src/metabase/browse/components/ModelsTable.styled.tsx
+++ b/frontend/src/metabase/browse/components/BrowseTable.styled.tsx
@@ -6,34 +6,45 @@ import {
   hideResponsively,
 } from "metabase/components/ItemsTable/BaseItemsTable.styled";
 import type { ResponsiveProps } from "metabase/components/ItemsTable/utils";
+import Link from "metabase/core/components/Link";
 import { breakpoints } from "metabase/ui/theme";
 
-export const ModelTableRow = styled.tr<{ skeleton?: boolean }>`
+export const TableRow = styled.tr<{ skeleton?: boolean }>`
   :focus {
     outline: 2px solid var(--mb-color-focus);
   }
   ${props =>
     props.skeleton
       ? `
-    :hover { background-color: unset ! important; }
-    td { cursor: unset ! important; }
-    `
+          :hover { background-color: unset ! important; }
+          td { cursor: unset ! important; }
+        `
       : `cursor: pointer;`}
 `;
 
-export const ModelNameLink = styled(ItemLink)`
+export const NameLink = styled(ItemLink)`
   padding-inline-start: 0.6rem;
   padding-block: 0.5rem;
 `;
 
-export const ModelCell = styled.td<ResponsiveProps>`
+export const Cell = styled.td<ResponsiveProps>`
   td& {
     padding: 0.25em 0.5rem 0.25em 0.5rem;
   }
+
+  &:focus-within,
+  &:focus {
+    outline: 2px solid var(--mb-color-focus);
+
+    a {
+      outline: none;
+    }
+  }
+
   ${hideResponsively}
 `;
 
-export const ModelNameColumn = styled(TableColumn)`
+export const NameColumn = styled(TableColumn)`
   width: 356px;
 
   @container ${props => props.containerName} (max-width: ${breakpoints.md}) {
@@ -44,3 +55,19 @@ export const ModelNameColumn = styled(TableColumn)`
     width: 200px;
   }
 `;
+
+export const CollectionTableCell = styled(Cell)`
+  td& {
+    padding: 0;
+  }
+`;
+
+export const CollectionLink = styled(Link)`
+  display: block;
+  padding: 1em 0.5em;
+  box-sizing: border-box;
+
+  &:hover {
+    color: var(--mb-color-icon-primary) !important;
+  }
+`;
diff --git a/frontend/src/metabase/browse/components/EllipsifiedWithMarkdownTooltip.tsx b/frontend/src/metabase/browse/components/EllipsifiedWithMarkdownTooltip.tsx
deleted file mode 100644
index fe3458f6314387573bc31de0fa1568f5f7bd8525..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/browse/components/EllipsifiedWithMarkdownTooltip.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { Ellipsified } from "metabase/core/components/Ellipsified";
-import Markdown from "metabase/core/components/Markdown";
-
-export const EllipsifiedWithMarkdownTooltip = ({
-  children,
-}: {
-  children: string;
-}) => {
-  return (
-    <Ellipsified
-      tooltip={
-        <Markdown disallowHeading unstyleLinks lineClamp={12}>
-          {children}
-        </Markdown>
-      }
-    >
-      {children}
-    </Ellipsified>
-  );
-};
diff --git a/frontend/src/metabase/browse/components/MetricsTable.tsx b/frontend/src/metabase/browse/components/MetricsTable.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..7a140e3bd1b20698b7d99194ecd0df47553a6c1e
--- /dev/null
+++ b/frontend/src/metabase/browse/components/MetricsTable.tsx
@@ -0,0 +1,295 @@
+import { type MouseEvent, useCallback, useState } from "react";
+import { push } from "react-router-redux";
+import { t } from "ttag";
+
+import { getCollectionName } from "metabase/collections/utils";
+import { EllipsifiedCollectionPath } from "metabase/common/components/EllipsifiedPath/EllipsifiedCollectionPath";
+import { useLocale } from "metabase/common/hooks/use-locale/use-locale";
+import EntityItem from "metabase/components/EntityItem";
+import { SortableColumnHeader } from "metabase/components/ItemsTable/BaseItemsTable";
+import {
+  ItemNameCell,
+  MaybeItemLink,
+  TBody,
+  Table,
+  TableColumn,
+} from "metabase/components/ItemsTable/BaseItemsTable.styled";
+import { Columns } from "metabase/components/ItemsTable/Columns";
+import type { ResponsiveProps } from "metabase/components/ItemsTable/utils";
+import { MarkdownPreview } from "metabase/core/components/MarkdownPreview";
+import { useDispatch } from "metabase/lib/redux";
+import * as Urls from "metabase/lib/urls";
+import { FixedSizeIcon, Flex, Skeleton } from "metabase/ui";
+import { Repeat } from "metabase/ui/components/feedback/Skeleton/Repeat";
+import { SortDirection, type SortingOptions } from "metabase-types/api/sorting";
+
+import type { MetricResult } from "../types";
+
+import {
+  Cell,
+  CollectionLink,
+  CollectionTableCell,
+  NameColumn,
+  TableRow,
+} from "./BrowseTable.styled";
+import { getMetricDescription, sortModelOrMetric } from "./utils";
+
+type MetricsTableProps = {
+  metrics?: MetricResult[];
+  skeleton?: boolean;
+};
+
+const DEFAULT_SORTING_OPTIONS: SortingOptions = {
+  sort_column: "name",
+  sort_direction: SortDirection.Asc,
+};
+
+export const itemsTableContainerName = "ItemsTableContainer";
+
+const nameProps = {
+  containerName: itemsTableContainerName,
+};
+
+const descriptionProps: ResponsiveProps = {
+  hideAtContainerBreakpoint: "sm",
+  containerName: itemsTableContainerName,
+};
+
+const collectionProps: ResponsiveProps = {
+  hideAtContainerBreakpoint: "xs",
+  containerName: itemsTableContainerName,
+};
+
+export function MetricsTable({
+  skeleton = false,
+  metrics = [],
+}: MetricsTableProps) {
+  const [sortingOptions, setSortingOptions] = useState<SortingOptions>(
+    DEFAULT_SORTING_OPTIONS,
+  );
+
+  const locale = useLocale();
+  const sortedMetrics = sortModelOrMetric(metrics, sortingOptions, locale);
+
+  const handleSortingOptionsChange = skeleton ? undefined : setSortingOptions;
+
+  /** The name column has an explicitly set width. The remaining columns divide the remaining width. This is the percentage allocated to the collection column */
+  const collectionWidth = 38.5;
+  const descriptionWidth = 100 - collectionWidth;
+
+  return (
+    <Table aria-label={skeleton ? undefined : t`Table of metrics`}>
+      <colgroup>
+        {/* <col> for Name column */}
+        <NameColumn {...nameProps} />
+
+        {/* <col> for Collection column */}
+        <TableColumn {...collectionProps} width={`${collectionWidth}%`} />
+
+        {/* <col> for Description column */}
+        <TableColumn {...descriptionProps} width={`${descriptionWidth}%`} />
+
+        <Columns.RightEdge.Col />
+      </colgroup>
+      <thead>
+        <tr>
+          <SortableColumnHeader
+            name="name"
+            sortingOptions={sortingOptions}
+            onSortingOptionsChange={handleSortingOptionsChange}
+            {...nameProps}
+            style={{ paddingInlineStart: ".625rem" }}
+            columnHeaderProps={{
+              style: { paddingInlineEnd: ".5rem" },
+            }}
+          >
+            {t`Name`}
+          </SortableColumnHeader>
+          <SortableColumnHeader
+            name="collection"
+            sortingOptions={sortingOptions}
+            onSortingOptionsChange={handleSortingOptionsChange}
+            {...collectionProps}
+            columnHeaderProps={{
+              style: {
+                paddingInline: ".5rem",
+              },
+            }}
+          >
+            {t`Collection`}
+          </SortableColumnHeader>
+          <SortableColumnHeader
+            name="description"
+            sortingOptions={sortingOptions}
+            onSortingOptionsChange={handleSortingOptionsChange}
+            {...descriptionProps}
+            columnHeaderProps={{
+              style: {
+                paddingInline: ".5rem",
+              },
+            }}
+          >
+            {t`Description`}
+          </SortableColumnHeader>
+          <Columns.RightEdge.Header />
+        </tr>
+      </thead>
+      <TBody>
+        {skeleton ? (
+          <Repeat times={7}>
+            <MetricRow />
+          </Repeat>
+        ) : (
+          sortedMetrics.map((metric: MetricResult) => (
+            <MetricRow metric={metric} key={metric.id} />
+          ))
+        )}
+      </TBody>
+    </Table>
+  );
+}
+
+function MetricRow({ metric }: { metric?: MetricResult }) {
+  const dispatch = useDispatch();
+
+  const handleClick = useCallback(
+    (event: MouseEvent) => {
+      if (!metric) {
+        return;
+      }
+
+      // do not trigger click when selecting text
+      const selection = document.getSelection();
+      if (selection?.type === "Range") {
+        event.stopPropagation();
+        return;
+      }
+
+      const { id, name } = metric;
+      const url = Urls.metric({ id, name });
+      const subpathSafeUrl = Urls.getSubpathSafeUrl(url);
+
+      // TODO: metabase/metabse#47713
+      // trackMetricClick(metric.id);
+
+      event.preventDefault();
+      event.stopPropagation();
+
+      if ((event.ctrlKey || event.metaKey) && event.button === 0) {
+        Urls.openInNewTab(subpathSafeUrl);
+      } else {
+        dispatch(push(url));
+      }
+    },
+    [metric, dispatch],
+  );
+
+  return (
+    <TableRow onClick={handleClick}>
+      <NameCell metric={metric} />
+      <CollectionCell metric={metric} />
+      <DescriptionCell metric={metric} />
+      <Columns.RightEdge.Cell />
+    </TableRow>
+  );
+}
+
+function SkeletonText() {
+  return <Skeleton natural h="16.8px" />;
+}
+
+function stopPropagation(event: MouseEvent) {
+  event.stopPropagation();
+}
+
+function preventDefault(event: MouseEvent) {
+  event.preventDefault();
+}
+
+function NameCell({ metric }: { metric?: MetricResult }) {
+  const headingId = `metric-${metric?.id ?? "dummy"}-heading`;
+
+  return (
+    <ItemNameCell
+      data-testid="metric-name"
+      aria-labelledby={headingId}
+      {...nameProps}
+    >
+      <MaybeItemLink
+        to={
+          metric ? Urls.metric({ id: metric.id, name: metric.name }) : undefined
+        }
+        style={{
+          // To align the icons with "Name" in the <th>
+          paddingInlineStart: "1.4rem",
+          paddingInlineEnd: ".5rem",
+        }}
+        onClick={preventDefault}
+      >
+        {metric ? (
+          <EntityItem.Name
+            name={metric?.name || ""}
+            variant="list"
+            id={headingId}
+          />
+        ) : (
+          <SkeletonText />
+        )}
+      </MaybeItemLink>
+    </ItemNameCell>
+  );
+}
+
+function CollectionCell({ metric }: { metric?: MetricResult }) {
+  const collectionName = metric?.collection
+    ? getCollectionName(metric.collection)
+    : t`Untitled collection`;
+
+  const content = (
+    <Flex gap="sm">
+      <FixedSizeIcon name="folder" />
+
+      {metric ? (
+        <EllipsifiedCollectionPath collection={metric.collection} />
+      ) : (
+        <SkeletonText />
+      )}
+    </Flex>
+  );
+
+  return (
+    <CollectionTableCell
+      data-testid={`path-for-collection: ${collectionName}`}
+      {...collectionProps}
+    >
+      {metric?.collection ? (
+        <CollectionLink
+          to={Urls.collection(metric.collection)}
+          onClick={stopPropagation}
+        >
+          {content}
+        </CollectionLink>
+      ) : (
+        content
+      )}
+    </CollectionTableCell>
+  );
+}
+
+function DescriptionCell({ metric }: { metric?: MetricResult }) {
+  return (
+    <Cell {...descriptionProps}>
+      {metric ? (
+        <MarkdownPreview
+          lineClamp={12}
+          allowedElements={["strong", "em"]}
+          oneLine
+        >
+          {getMetricDescription(metric) || ""}
+        </MarkdownPreview>
+      ) : (
+        <SkeletonText />
+      )}
+    </Cell>
+  );
+}
diff --git a/frontend/src/metabase/browse/components/ModelsTable.module.css b/frontend/src/metabase/browse/components/ModelsTable.module.css
deleted file mode 100644
index f29119d547afe2c2cd394305494d1847877b0312..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/browse/components/ModelsTable.module.css
+++ /dev/null
@@ -1,5 +0,0 @@
-.collectionLink {
-  :hover {
-    color: var(--mb-color-brand) !important;
-  }
-}
diff --git a/frontend/src/metabase/browse/components/ModelsTable.tsx b/frontend/src/metabase/browse/components/ModelsTable.tsx
index 5ce8f905ca18cc9439d76dea9b70886d03c4fcc4..815dd8619795274863ab907bac71b803fa17a9bf 100644
--- a/frontend/src/metabase/browse/components/ModelsTable.tsx
+++ b/frontend/src/metabase/browse/components/ModelsTable.tsx
@@ -1,4 +1,4 @@
-import { type CSSProperties, type PropsWithChildren, useState } from "react";
+import { type MouseEvent, useCallback, useState } from "react";
 import { push } from "react-router-redux";
 import { t } from "ttag";
 
@@ -17,18 +17,10 @@ import {
 import { Columns } from "metabase/components/ItemsTable/Columns";
 import type { ResponsiveProps } from "metabase/components/ItemsTable/utils";
 import { Ellipsified } from "metabase/core/components/Ellipsified";
-import Link from "metabase/core/components/Link";
+import { MarkdownPreview } from "metabase/core/components/MarkdownPreview";
 import { useDispatch } from "metabase/lib/redux";
 import * as Urls from "metabase/lib/urls";
-import {
-  Box,
-  FixedSizeIcon,
-  Flex,
-  Icon,
-  type IconName,
-  type IconProps,
-  Skeleton,
-} from "metabase/ui";
+import { FixedSizeIcon, Flex, Icon, Skeleton } from "metabase/ui";
 import { Repeat } from "metabase/ui/components/feedback/Skeleton/Repeat";
 import { SortDirection, type SortingOptions } from "metabase-types/api/sorting";
 
@@ -36,14 +28,14 @@ import { trackModelClick } from "../analytics";
 import type { ModelResult } from "../types";
 import { getIcon } from "../utils";
 
-import { EllipsifiedWithMarkdownTooltip } from "./EllipsifiedWithMarkdownTooltip";
-import S from "./ModelsTable.module.css";
 import {
-  ModelCell,
-  ModelNameColumn,
-  ModelTableRow,
-} from "./ModelsTable.styled";
-import { getModelDescription, sortModels } from "./utils";
+  Cell,
+  CollectionLink,
+  CollectionTableCell,
+  NameColumn,
+  TableRow,
+} from "./BrowseTable.styled";
+import { getModelDescription, sortModelOrMetric } from "./utils";
 
 export interface ModelsTableProps {
   models?: ModelResult[];
@@ -77,7 +69,7 @@ export const ModelsTable = ({
   );
 
   const locale = useLocale();
-  const sortedModels = sortModels(models, sortingOptions, locale);
+  const sortedModels = sortModelOrMetric(models, sortingOptions, locale);
 
   /** The name column has an explicitly set width. The remaining columns divide the remaining width. This is the percentage allocated to the collection column */
   const collectionWidth = 38.5;
@@ -93,7 +85,7 @@ export const ModelsTable = ({
     <Table aria-label={skeleton ? undefined : t`Table of models`}>
       <colgroup>
         {/* <col> for Name column */}
-        <ModelNameColumn containerName={itemsTableContainerName} />
+        <NameColumn containerName={itemsTableContainerName} />
 
         {/* <col> for Collection column */}
         <TableColumn {...collectionProps} width={`${collectionWidth}%`} />
@@ -146,11 +138,11 @@ export const ModelsTable = ({
       <TBody>
         {skeleton ? (
           <Repeat times={7}>
-            <TBodyRowSkeleton />
+            <ModelRow />
           </Repeat>
         ) : (
           sortedModels.map((model: ModelResult) => (
-            <TBodyRow model={model} key={`${model.model}-${model.id}`} />
+            <ModelRow model={model} key={model.id} />
           ))
         )}
       </TBody>
@@ -158,156 +150,144 @@ export const ModelsTable = ({
   );
 };
 
-const TBodyRow = ({
-  model,
-  skeleton,
-}: {
-  model: ModelResult;
-  skeleton?: boolean;
-}) => {
-  const icon = getIcon(model);
+function SkeletonText() {
+  return <Skeleton natural h="16.8px" />;
+}
+
+function stopPropagation(event: MouseEvent) {
+  event.stopPropagation();
+}
+
+function preventDefault(event: MouseEvent) {
+  event.preventDefault();
+}
+
+const ModelRow = ({ model }: { model?: ModelResult }) => {
   const dispatch = useDispatch();
-  const { id, name } = model;
 
-  return (
-    <ModelTableRow
-      onClick={(e: React.MouseEvent) => {
-        if (skeleton) {
-          return;
-        }
-        const url = Urls.model({ id, name });
-        const subpathSafeUrl = Urls.getSubpathSafeUrl(url);
+  const handleClick = useCallback(
+    (event: MouseEvent) => {
+      if (!model) {
+        return;
+      }
 
-        if ((e.ctrlKey || e.metaKey) && e.button === 0) {
-          Urls.openInNewTab(subpathSafeUrl);
-        } else {
-          dispatch(push(url));
-        }
-      }}
-      tabIndex={0}
-      key={model.id}
-    >
-      {/* Name */}
-      <NameCell
-        model={model}
-        icon={icon}
-        onClick={() => {
-          if (skeleton) {
-            return;
-          }
-          trackModelClick(model.id);
-        }}
-      />
-
-      {/* Collection */}
-      <ModelCell
-        data-testid={`path-for-collection: ${
-          model.collection
-            ? getCollectionName(model.collection)
-            : t`Untitled collection`
-        }`}
-        {...collectionProps}
-      >
-        <Link
-          className={S.collectionLink}
-          to={Urls.collection(model.collection)}
-          onClick={e => e.stopPropagation()}
-        >
-          <Flex gap="sm">
-            <FixedSizeIcon name="folder" />
-            <Box w="calc(100% - 1.5rem)">
-              <EllipsifiedCollectionPath collection={model.collection} />
-            </Box>
-          </Flex>
-        </Link>
-      </ModelCell>
-
-      {/* Description */}
-      <ModelCell {...descriptionProps}>
-        <EllipsifiedWithMarkdownTooltip>
-          {getModelDescription(model) || ""}
-        </EllipsifiedWithMarkdownTooltip>
-      </ModelCell>
+      // do not trigger click when selecting text
+      const selection = document.getSelection();
+      if (selection?.type === "Range") {
+        event.stopPropagation();
+        return;
+      }
+
+      const { id, name } = model;
+      const url = Urls.model({ id, name });
+      const subpathSafeUrl = Urls.getSubpathSafeUrl(url);
+
+      trackModelClick(model.id);
+
+      event.preventDefault();
+      event.stopPropagation();
+
+      if ((event.ctrlKey || event.metaKey) && event.button === 0) {
+        Urls.openInNewTab(subpathSafeUrl);
+      } else {
+        dispatch(push(url));
+      }
+    },
+    [model, dispatch],
+  );
 
-      {/* Adds a border-radius to the table */}
+  return (
+    <TableRow onClick={handleClick}>
+      <NameCell model={model} />
+      <CollectionCell model={model} />
+      <DescriptionCell model={model} />
       <Columns.RightEdge.Cell />
-    </ModelTableRow>
+    </TableRow>
   );
 };
 
-const NameCell = ({
-  model,
-  testIdPrefix = "table",
-  onClick,
-  icon,
-  children,
-}: PropsWithChildren<{
-  model?: ModelResult;
-  testIdPrefix?: string;
-  onClick?: () => void;
-  icon: IconProps;
-}>) => {
+function NameCell({ model }: { model?: ModelResult }) {
   const headingId = `model-${model?.id || "dummy"}-heading`;
+  const icon = getIcon(model);
   return (
-    <ItemNameCell
-      data-testid={`${testIdPrefix}-name`}
-      aria-labelledby={headingId}
-    >
+    <ItemNameCell data-testid="model-name" aria-labelledby={headingId}>
       <MaybeItemLink
         to={model ? Urls.model({ id: model.id, name: model.name }) : undefined}
-        onClick={onClick}
         style={{
           // To align the icons with "Name" in the <th>
           paddingInlineStart: "1.4rem",
           paddingInlineEnd: ".5rem",
         }}
+        onClick={preventDefault}
       >
         <Icon
           size={16}
           {...icon}
-          color={"var(--mb-color-brand)"}
+          color="var(--mb-color-icon-primary)"
           style={{ flexShrink: 0 }}
         />
-        {children || (
+        {
           <EntityItem.Name
             name={model?.name || ""}
             variant="list"
             id={headingId}
           />
-        )}
+        }
       </MaybeItemLink>
     </ItemNameCell>
   );
-};
+}
 
-const CellTextSkeleton = () => {
-  return <Skeleton natural h="16.8px" />;
-};
+function CollectionCell({ model }: { model?: ModelResult }) {
+  const collectionName = model?.collection
+    ? getCollectionName(model.collection)
+    : t`Untitled collection`;
+
+  const content = (
+    <Flex gap="sm">
+      <FixedSizeIcon name="folder" />
+
+      {model ? (
+        <EllipsifiedCollectionPath collection={model.collection} />
+      ) : (
+        <SkeletonText />
+      )}
+    </Flex>
+  );
 
-const TBodyRowSkeleton = ({ style }: { style?: CSSProperties }) => {
-  const icon = { name: "model" as IconName };
   return (
-    <ModelTableRow skeleton style={style}>
-      {/* Name */}
-      <NameCell icon={icon}>
-        <CellTextSkeleton />
-      </NameCell>
-
-      {/* Collection */}
-      <ModelCell {...collectionProps}>
-        <Flex gap=".5rem">
-          <FixedSizeIcon name="folder" />
-          <CellTextSkeleton />
-        </Flex>
-      </ModelCell>
-
-      {/* Description */}
-      <ModelCell {...descriptionProps}>
-        <CellTextSkeleton />
-      </ModelCell>
-
-      {/* Adds a border-radius to the table */}
-      <Columns.RightEdge.Cell />
-    </ModelTableRow>
+    <CollectionTableCell
+      data-testid={`path-for-collection: ${collectionName}`}
+      {...collectionProps}
+    >
+      {model?.collection ? (
+        <CollectionLink
+          to={Urls.collection(model.collection)}
+          onClick={stopPropagation}
+        >
+          {content}
+        </CollectionLink>
+      ) : (
+        content
+      )}
+    </CollectionTableCell>
   );
-};
+}
+
+function DescriptionCell({ model }: { model?: ModelResult }) {
+  return (
+    <Cell {...descriptionProps}>
+      {model ? (
+        <MarkdownPreview
+          lineClamp={12}
+          allowedElements={["strong", "em"]}
+          oneLine
+        >
+          {getModelDescription(model) || ""}
+        </MarkdownPreview>
+      ) : (
+        <SkeletonText />
+      )}
+    </Cell>
+  );
+}
diff --git a/frontend/src/metabase/browse/components/utils.tsx b/frontend/src/metabase/browse/components/utils.tsx
index 2aa67029d5bf686dc7d49eb71f8f2dabc5a29f9d..3b0c9a34b401c6059c2fc655ffa220db7fc6efd7 100644
--- a/frontend/src/metabase/browse/components/utils.tsx
+++ b/frontend/src/metabase/browse/components/utils.tsx
@@ -4,33 +4,45 @@ import { getCollectionPathAsString } from "metabase/collections/utils";
 import type { SearchResult } from "metabase-types/api";
 import { SortDirection, type SortingOptions } from "metabase-types/api/sorting";
 
-import type { ModelResult } from "../types";
+import type { MetricResult, ModelResult } from "../types";
+
+export type ModelOrMetricResult = ModelResult | MetricResult;
 
 export const isModel = (item: SearchResult) => item.model === "dataset";
 
-export const getModelDescription = (item: SearchResult) => {
-  if (item.collection && isModel(item) && !item.description?.trim()) {
+export const getModelDescription = (item: ModelResult) => {
+  if (item.collection && !item.description?.trim()) {
     return t`A model`;
   } else {
     return item.description;
   }
 };
 
+export const isMetric = (item: SearchResult) => item.model === "metric";
+
+export const getMetricDescription = (item: MetricResult) => {
+  if (item.collection && !item.description?.trim()) {
+    return t`A metric`;
+  }
+
+  return item.description;
+};
+
 const getValueForSorting = (
-  model: ModelResult,
+  model: ModelResult | MetricResult,
   sort_column: keyof ModelResult,
 ): string => {
   if (sort_column === "collection") {
-    return getCollectionPathAsString(model.collection);
+    return getCollectionPathAsString(model.collection) ?? "";
   } else {
-    return model[sort_column];
+    return model[sort_column] ?? "";
   }
 };
 
 export const isValidSortColumn = (
   sort_column: string,
 ): sort_column is keyof ModelResult => {
-  return ["name", "collection"].includes(sort_column);
+  return ["name", "collection", "description"].includes(sort_column);
 };
 
 export const getSecondarySortColumn = (
@@ -39,36 +51,36 @@ export const getSecondarySortColumn = (
   return sort_column === "name" ? "collection" : "name";
 };
 
-export const sortModels = (
-  models: ModelResult[],
+export function sortModelOrMetric<T extends ModelOrMetricResult>(
+  modelsOrMetrics: T[],
   sortingOptions: SortingOptions,
   localeCode: string = "en",
-) => {
+) {
   const { sort_column, sort_direction } = sortingOptions;
 
   if (!isValidSortColumn(sort_column)) {
     console.error("Invalid sort column", sort_column);
-    return models;
+    return modelsOrMetrics;
   }
 
   const compare = (a: string, b: string) =>
     a.localeCompare(b, localeCode, { sensitivity: "base" });
 
-  return [...models].sort((modelA, modelB) => {
-    const a = getValueForSorting(modelA, sort_column);
-    const b = getValueForSorting(modelB, sort_column);
+  return [...modelsOrMetrics].sort((modelOrMetricA, modelOrMetricB) => {
+    const a = getValueForSorting(modelOrMetricA, sort_column);
+    const b = getValueForSorting(modelOrMetricB, sort_column);
 
     let result = compare(a, b);
     if (result === 0) {
       const sort_column2 = getSecondarySortColumn(sort_column);
-      const a2 = getValueForSorting(modelA, sort_column2);
-      const b2 = getValueForSorting(modelB, sort_column2);
+      const a2 = getValueForSorting(modelOrMetricA, sort_column2);
+      const b2 = getValueForSorting(modelOrMetricB, sort_column2);
       result = compare(a2, b2);
     }
 
     return sort_direction === SortDirection.Asc ? result : -result;
   });
-};
+}
 
 /** Find the maximum number of recently viewed models to show.
  * This is roughly proportional to the number of models the user
diff --git a/frontend/src/metabase/browse/components/utils.unit.spec.tsx b/frontend/src/metabase/browse/components/utils.unit.spec.tsx
index 80cd31e2972b9fbaebee830373c8aef4d76ff9df..8039bab10611a432ae7b5d59a6c03c1da8a45903 100644
--- a/frontend/src/metabase/browse/components/utils.unit.spec.tsx
+++ b/frontend/src/metabase/browse/components/utils.unit.spec.tsx
@@ -4,7 +4,7 @@ import { SortDirection } from "metabase-types/api/sorting";
 import { createMockModelResult } from "../test-utils";
 import type { ModelResult } from "../types";
 
-import { getMaxRecentModelCount, sortModels } from "./utils";
+import { getMaxRecentModelCount, sortModelOrMetric } from "./utils";
 
 describe("sortModels", () => {
   let id = 0;
@@ -44,7 +44,7 @@ describe("sortModels", () => {
       sort_column: "name",
       sort_direction: SortDirection.Asc,
     } as const;
-    const sorted = sortModels(mockSearchResults, sortingOptions);
+    const sorted = sortModelOrMetric(mockSearchResults, sortingOptions);
     expect(sorted?.map(model => model.name)).toEqual(["A", "B", "C"]);
   });
 
@@ -53,7 +53,7 @@ describe("sortModels", () => {
       sort_column: "name",
       sort_direction: SortDirection.Desc,
     } as const;
-    const sorted = sortModels(mockSearchResults, sortingOptions);
+    const sorted = sortModelOrMetric(mockSearchResults, sortingOptions);
     expect(sorted?.map(model => model.name)).toEqual(["C", "B", "A"]);
   });
 
@@ -62,7 +62,7 @@ describe("sortModels", () => {
       sort_column: "collection",
       sort_direction: SortDirection.Asc,
     } as const;
-    const sorted = sortModels(mockSearchResults, sortingOptions);
+    const sorted = sortModelOrMetric(mockSearchResults, sortingOptions);
     expect(sorted?.map(model => model.name)).toEqual(["B", "A", "C"]);
   });
 
@@ -71,7 +71,7 @@ describe("sortModels", () => {
       sort_column: "collection",
       sort_direction: SortDirection.Desc,
     } as const;
-    const sorted = sortModels(mockSearchResults, sortingOptions);
+    const sorted = sortModelOrMetric(mockSearchResults, sortingOptions);
     expect(sorted?.map(model => model.name)).toEqual(["C", "A", "B"]);
   });
 
@@ -98,7 +98,7 @@ describe("sortModels", () => {
         sort_column: "collection",
         sort_direction: SortDirection.Asc,
       } as const;
-      const sorted = sortModels(mockSearchResults, sortingOptions);
+      const sorted = sortModelOrMetric(mockSearchResults, sortingOptions);
       expect(sorted).toEqual([
         modelMap["model named B, with collection path D / E / F"],
         modelMap["model named Bz, with collection path D / E / F"],
@@ -113,7 +113,7 @@ describe("sortModels", () => {
         sort_column: "collection",
         sort_direction: SortDirection.Desc,
       } as const;
-      const sorted = sortModels(mockSearchResults, sortingOptions);
+      const sorted = sortModelOrMetric(mockSearchResults, sortingOptions);
       expect(sorted).toEqual([
         modelMap["model named C, with collection path Z"],
         modelMap["model named C, with collection path Y"],
@@ -165,7 +165,7 @@ describe("sortModels", () => {
 
       // When sorting in Swedish, z comes before ä
       const swedishLocaleCode = "sv";
-      const sorted = sortModels(
+      const sorted = sortModelOrMetric(
         swedishResults,
         sortingOptions,
         swedishLocaleCode,
diff --git a/frontend/src/metabase/browse/test-utils.ts b/frontend/src/metabase/browse/test-utils.ts
index c3b0e8c7635df326386a8be0673f193d902d16ee..38ee0947246e2cabf0ed92458fb8c0b0d4a5e1d3 100644
--- a/frontend/src/metabase/browse/test-utils.ts
+++ b/frontend/src/metabase/browse/test-utils.ts
@@ -4,7 +4,12 @@ import {
   createMockSearchResult,
 } from "metabase-types/api/mocks";
 
-import type { ModelResult, RecentModel } from "./types";
+import type {
+  MetricResult,
+  ModelResult,
+  RecentMetric,
+  RecentModel,
+} from "./types";
 
 export const createMockModelResult = (
   model: Partial<ModelResult> = {},
@@ -15,3 +20,16 @@ export const createMockRecentModel = (
   model: Partial<RecentCollectionItem>,
 ): RecentModel =>
   createMockRecentCollectionItem({ ...model, model: "dataset" }) as RecentModel;
+
+export const createMockMetricResult = (
+  metric: Partial<MetricResult> = {},
+): MetricResult =>
+  createMockSearchResult({ ...metric, model: "metric" }) as MetricResult;
+
+export const createMockRecentMetric = (
+  metric: Partial<RecentMetric>,
+): RecentMetric =>
+  createMockRecentCollectionItem({
+    ...metric,
+    model: "metric",
+  }) as RecentMetric;
diff --git a/frontend/src/metabase/browse/types.tsx b/frontend/src/metabase/browse/types.tsx
index d689a551985ae0832eebe869ff854ea131e35194..7ecc3a879b6ab0dc61742e997a7a8746788412ee 100644
--- a/frontend/src/metabase/browse/types.tsx
+++ b/frontend/src/metabase/browse/types.tsx
@@ -19,3 +19,10 @@ export const isRecentModel = (item: RecentItem): item is RecentModel =>
  * This type is needed so that our filtering functions can
  * filter arrays of models retrieved from either endpoint. */
 export type FilterableModel = ModelResult | RecentModel;
+
+/** Metric retrieved through the search endpoint */
+export type MetricResult = SearchResult<number, "metric">;
+
+export interface RecentMetric extends RecentCollectionItem {
+  model: "metric";
+}
diff --git a/frontend/src/metabase/common/hooks/use-fetch-metrics.tsx b/frontend/src/metabase/common/hooks/use-fetch-metrics.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..c8bcc6944a9385cd922e4bed654052503156ca32
--- /dev/null
+++ b/frontend/src/metabase/common/hooks/use-fetch-metrics.tsx
@@ -0,0 +1,10 @@
+import { useSearchQuery } from "metabase/api";
+import type { SearchRequest } from "metabase-types/api";
+
+export const useFetchMetrics = (req: Partial<SearchRequest> = {}) => {
+  const modelsResult = useSearchQuery({
+    models: ["metric"],
+    ...req,
+  });
+  return modelsResult;
+};
diff --git a/frontend/src/metabase/core/components/MarkdownPreview/MarkdownPreview.module.css b/frontend/src/metabase/core/components/MarkdownPreview/MarkdownPreview.module.css
new file mode 100644
index 0000000000000000000000000000000000000000..b9fb0044f96f48dadcb4972032c6bcc48c89f00d
--- /dev/null
+++ b/frontend/src/metabase/core/components/MarkdownPreview/MarkdownPreview.module.css
@@ -0,0 +1,47 @@
+.preview {
+  address,
+  article,
+  aside,
+  blockquote,
+  canvas,
+  dd,
+  div,
+  dl,
+  dt,
+  fieldset,
+  figcaption,
+  figure,
+  footer,
+  form,
+  h1,
+  h2,
+  h3,
+  h4,
+  h5,
+  h6,
+  header,
+  hr,
+  li,
+  main,
+  nav,
+  noscript,
+  ol,
+  p,
+  pre,
+  section,
+  table,
+  tfoot,
+  ul,
+  video {
+    /**
+     * The truncation detection only works when all the elements in the markdown preview
+     * are inline. Because we cannot control the types of elements being rendered, we force them all to
+     * inline here.
+     */
+    display: inline-block;
+  }
+}
+
+.preview.oneLine {
+  white-space: normal;
+}
diff --git a/frontend/src/metabase/core/components/MarkdownPreview/MarkdownPreview.tsx b/frontend/src/metabase/core/components/MarkdownPreview/MarkdownPreview.tsx
index e85346a4a4c66b6c82e8d9724b4d36811849556b..36762dd2f73f182a87b4cac983bb627279aa2ff3 100644
--- a/frontend/src/metabase/core/components/MarkdownPreview/MarkdownPreview.tsx
+++ b/frontend/src/metabase/core/components/MarkdownPreview/MarkdownPreview.tsx
@@ -1,3 +1,4 @@
+import classNames from "classnames";
 import type { ComponentProps, LegacyRef } from "react";
 
 import { useIsTruncated } from "metabase/hooks/use-is-truncated";
@@ -5,18 +6,26 @@ import { useIsTruncated } from "metabase/hooks/use-is-truncated";
 import Markdown from "../Markdown";
 import Tooltip from "../Tooltip";
 
+import C from "./MarkdownPreview.module.css";
+
 interface Props {
   children: string;
   className?: string;
   tooltipMaxWidth?: ComponentProps<typeof Tooltip>["maxWidth"];
+  lineClamp?: number;
+  allowedElements?: string[];
+  oneLine?: boolean;
 }
 
-const ALLOWED_ELEMENTS: string[] = [];
+const DEFAULT_ALLOWED_ELEMENTS: string[] = [];
 
 export const MarkdownPreview = ({
   children,
   className,
   tooltipMaxWidth,
+  lineClamp,
+  allowedElements = DEFAULT_ALLOWED_ELEMENTS,
+  oneLine,
 }: Props) => {
   const { isTruncated, ref } = useIsTruncated();
 
@@ -35,15 +44,15 @@ export const MarkdownPreview = ({
       placement="bottom"
       isEnabled={isTruncated}
       tooltip={
-        <Markdown dark disallowHeading unstyleLinks>
+        <Markdown dark disallowHeading unstyleLinks lineClamp={lineClamp}>
           {children}
         </Markdown>
       }
     >
       <div ref={setReactMarkdownRef}>
         <Markdown
-          allowedElements={ALLOWED_ELEMENTS}
-          className={className}
+          allowedElements={allowedElements}
+          className={classNames(C.preview, className, oneLine && C.oneLine)}
           unwrapDisallowed
           lineClamp={1}
         >
diff --git a/frontend/src/metabase/core/components/MarkdownPreview/MarkdownPreview.unit.spec.tsx b/frontend/src/metabase/core/components/MarkdownPreview/MarkdownPreview.unit.spec.tsx
index 1f56c2f49730780effb683edbd879b10ddb58b75..624ca9f344e0640f5732fc1aada07a581fff1b5b 100644
--- a/frontend/src/metabase/core/components/MarkdownPreview/MarkdownPreview.unit.spec.tsx
+++ b/frontend/src/metabase/core/components/MarkdownPreview/MarkdownPreview.unit.spec.tsx
@@ -19,13 +19,19 @@ const MARKDOWN = [
 const MARKDOWN_AS_TEXT = [HEADING_1_TEXT, HEADING_2_TEXT, PARAGRAPH_TEXT].join(
   " ",
 );
+const MARKDOWN_WITH_ITALICS = `markdown with *italics*`;
 
 interface SetupOpts {
   markdown?: string;
+  allowedElements?: string[];
 }
 
-const setup = ({ markdown = MARKDOWN }: SetupOpts = {}) => {
-  render(<MarkdownPreview>{markdown}</MarkdownPreview>);
+const setup = ({ markdown = MARKDOWN, allowedElements }: SetupOpts = {}) => {
+  render(
+    <MarkdownPreview allowedElements={allowedElements}>
+      {markdown}
+    </MarkdownPreview>,
+  );
 };
 
 describe("MarkdownPreview", () => {
@@ -80,4 +86,13 @@ describe("MarkdownPreview", () => {
       expect(image).toHaveAttribute("src", "https://example.com/img.jpg");
     });
   });
+
+  it("should allow enabling certain elements", () => {
+    setup({
+      markdown: MARKDOWN_WITH_ITALICS,
+      allowedElements: ["em"],
+    });
+
+    expect(screen.getByText("italics")).toBeInTheDocument();
+  });
 });
diff --git a/frontend/src/metabase/css/core/colors.module.css b/frontend/src/metabase/css/core/colors.module.css
index 87ec497a2d4d982b49a0f7f5728d92c21e491b36..e4cb6af767510b0983b623be4fbf6bf57fdbbf7e 100644
--- a/frontend/src/metabase/css/core/colors.module.css
+++ b/frontend/src/metabase/css/core/colors.module.css
@@ -71,6 +71,7 @@
   --mb-color-background-disabled: var(--mb-base-color-gray-10);
   --mb-color-background-inverse: var(--mb-color-bg-black);
   --mb-color-background-brand: var(--mb-base-color-brand-40);
+  --mb-color-icon-primary: var(--mb-base-color-brand-40);
 
   /**
     * Base colors
diff --git a/frontend/src/metabase/nav/containers/MainNavbar/MainNavbarContainer/BrowseNavSection.tsx b/frontend/src/metabase/nav/containers/MainNavbar/MainNavbarContainer/BrowseNavSection.tsx
index 7103883592129971b0d7a01dfc3c52f3c8ad0d2f..e599866d5e71cd2a210b7caa8c1a425fefe81bc0 100644
--- a/frontend/src/metabase/nav/containers/MainNavbar/MainNavbarContainer/BrowseNavSection.tsx
+++ b/frontend/src/metabase/nav/containers/MainNavbar/MainNavbarContainer/BrowseNavSection.tsx
@@ -9,6 +9,7 @@ import { Flex, Skeleton } from "metabase/ui";
 
 import { PaddedSidebarLink, SidebarHeading } from "../MainNavbar.styled";
 import type { SelectedItem } from "../types";
+import { useHasMetrics } from "../utils";
 
 export const BrowseNavSection = ({
   nonEntityItem,
@@ -21,6 +22,7 @@ export const BrowseNavSection = ({
 }) => {
   const BROWSE_MODELS_URL = "/browse/models";
   const BROWSE_DATA_URL = "/browse/databases";
+  const BROWSE_METRICS_URL = "/browse/metrics";
 
   const {
     hasModels,
@@ -29,11 +31,18 @@ export const BrowseNavSection = ({
   } = useHasModels();
   const noModelsExist = hasModels === false;
 
+  const {
+    hasMetrics,
+    isLoading: areMetricsLoading,
+    error: metricsError,
+  } = useHasMetrics();
+  const noMetricsExist = hasMetrics === false;
+
   const [expandBrowse = true, setExpandBrowse] = useUserSetting(
     "expand-browse-in-nav",
   );
 
-  if (noModelsExist && !hasDataAccess) {
+  if (noModelsExist && noMetricsExist && !hasDataAccess) {
     return null;
   }
 
@@ -52,12 +61,7 @@ export const BrowseNavSection = ({
       <DelayedLoadingAndErrorWrapper
         loading={areModelsLoading}
         error={modelsError}
-        loader={
-          <Flex py="sm" px="md" h="32.67px" gap="sm" align="center">
-            <Skeleton radius="md" h="md" w="md" />
-            <Skeleton radius="xs" w="4rem" h="1.2rem" />
-          </Flex>
-        }
+        loader={<SidebarSkeleton />}
         delay={0}
       >
         {!noModelsExist && (
@@ -83,6 +87,34 @@ export const BrowseNavSection = ({
           {t`Databases`}
         </PaddedSidebarLink>
       )}
+
+      <DelayedLoadingAndErrorWrapper
+        loading={areMetricsLoading}
+        error={metricsError}
+        loader={<SidebarSkeleton />}
+        delay={0}
+      >
+        {!noMetricsExist && (
+          <PaddedSidebarLink
+            icon="metric"
+            url={BROWSE_METRICS_URL}
+            isSelected={nonEntityItem?.url?.startsWith(BROWSE_METRICS_URL)}
+            onClick={onItemSelect}
+            aria-label={t`Browse metrics`}
+          >
+            {t`Metrics`}
+          </PaddedSidebarLink>
+        )}
+      </DelayedLoadingAndErrorWrapper>
     </CollapseSection>
   );
 };
+
+function SidebarSkeleton() {
+  return (
+    <Flex py="sm" px="md" h="32.67px" gap="sm" align="center">
+      <Skeleton radius="md" h="md" w="md" />
+      <Skeleton radius="xs" w="4rem" h="1.2rem" />
+    </Flex>
+  );
+}
diff --git a/frontend/src/metabase/nav/containers/MainNavbar/utils/index.ts b/frontend/src/metabase/nav/containers/MainNavbar/utils/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fcfd614989089b0c1cf015a2f540da97facbf146
--- /dev/null
+++ b/frontend/src/metabase/nav/containers/MainNavbar/utils/index.ts
@@ -0,0 +1 @@
+export * from "./useHasMetrics";
diff --git a/frontend/src/metabase/nav/containers/MainNavbar/utils/useHasMetrics.ts b/frontend/src/metabase/nav/containers/MainNavbar/utils/useHasMetrics.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d9c8fdf744406dd53441aba5f4fa51824a141123
--- /dev/null
+++ b/frontend/src/metabase/nav/containers/MainNavbar/utils/useHasMetrics.ts
@@ -0,0 +1,20 @@
+import { useFetchMetrics } from "metabase/common/hooks/use-fetch-metrics";
+import type { SearchRequest } from "metabase-types/api";
+
+export const useHasMetrics = (req: Partial<SearchRequest> = {}) => {
+  const { data, isLoading, error } = useFetchMetrics({
+    limit: 0,
+    filter_items_in_personal_collection: "exclude",
+    model_ancestors: false,
+    ...req,
+  });
+
+  const availableModels = data?.available_models ?? [];
+  const hasMetrics = availableModels.includes("metric");
+
+  return {
+    hasMetrics,
+    isLoading,
+    error,
+  };
+};
diff --git a/frontend/src/metabase/routes.jsx b/frontend/src/metabase/routes.jsx
index 0199f21d114eede8183edde6249973b94d398c3f..8955d21d482482f288628f994b87b23b3533c611 100644
--- a/frontend/src/metabase/routes.jsx
+++ b/frontend/src/metabase/routes.jsx
@@ -52,6 +52,7 @@ import { Setup } from "metabase/setup/components/Setup";
 import getCollectionTimelineRoutes from "metabase/timelines/collections/routes";
 
 import { BrowseDatabases } from "./browse/components/BrowseDatabases";
+import { BrowseMetrics } from "./browse/components/BrowseMetrics";
 import { BrowseModels } from "./browse/components/BrowseModels";
 import BrowseSchemas from "./browse/components/BrowseSchemas";
 import { BrowseTables } from "./browse/components/BrowseTables";
@@ -206,6 +207,7 @@ export const getRoutes = store => {
 
           <Route path="browse">
             <IndexRedirect to="/browse/models" />
+            <Route path="metrics" component={BrowseMetrics} />
             <Route path="models" component={BrowseModels} />
             <Route path="databases" component={BrowseDatabases} />
             <Route path="databases/:slug" component={BrowseSchemas} />
diff --git a/resources/frontend_client/app/assets/img/metrics_bot.svg b/resources/frontend_client/app/assets/img/metrics_bot.svg
new file mode 100644
index 0000000000000000000000000000000000000000..6e845dcb5e0dd7cbb73b319e428f35d1dcbaa7d4
--- /dev/null
+++ b/resources/frontend_client/app/assets/img/metrics_bot.svg
@@ -0,0 +1,25 @@
+<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_8327_1165)">
+<circle cx="48" cy="48" r="48" fill="#4C5773" fill-opacity="0.05"/>
+<rect x="24" y="31" width="48" height="34" rx="4" fill="white" stroke="#949AAB" stroke-width="1.25" stroke-linejoin="round"/>
+<rect x="29.5" y="36.5" width="37" height="20" rx="1.5" fill="white" stroke="#949AAB" stroke-width="1.25" stroke-linejoin="round"/>
+<path d="M62.1605 60.7143C62.1605 61.3159 61.6728 61.8036 61.0712 61.8036C60.4696 61.8036 59.9819 61.3159 59.9819 60.7143C59.9819 60.1127 60.4696 59.625 61.0712 59.625C61.6728 59.625 62.1605 60.1127 62.1605 60.7143Z" stroke="#949AAB" stroke-width="1.25"/>
+<path d="M66.6605 60.7143C66.6605 61.3159 66.1728 61.8036 65.5712 61.8036C64.9696 61.8036 64.4819 61.3159 64.4819 60.7143C64.4819 60.1127 64.9696 59.625 65.5712 59.625C66.1728 59.625 66.6605 60.1127 66.6605 60.7143Z" stroke="#949AAB" stroke-width="1.25"/>
+<circle cx="41" cy="43.4996" r="1.5" fill="#949AAB"/>
+<circle cx="55" cy="43.4996" r="1.5" fill="#949AAB"/>
+<path d="M50.25 47.4996C50.25 48.9907 49.2426 49.7496 48 49.7496C46.7574 49.7496 45.75 48.9907 45.75 47.4996" stroke="#949AAB" stroke-width="1.25"/>
+<rect x="58.4795" y="26.667" width="24" height="16" transform="rotate(15 58.4795 26.667)" fill="black" fill-opacity="0.1"/>
+<rect x="58.4795" y="22.667" width="24" height="16" transform="rotate(15 58.4795 22.667)" fill="white" stroke="#949AAB" stroke-width="1.25" stroke-linejoin="round"/>
+<path opacity="0.5" d="M58.9786 36.259L63.3415 32.9911L64.9128 35.2609L68.9983 29.7745L70.5697 33.6711L72.9591 34.3113L76.7258 31.8448" stroke="#949AAB" stroke-width="1.25" stroke-linejoin="round"/>
+<path d="M14.7071 63.2929L31.2929 46.7071C31.9229 46.0771 33 46.5233 33 47.4142V64C33 64.5523 32.5523 65 32 65H15.4142C14.5233 65 14.0771 63.9229 14.7071 63.2929Z" fill="black" fill-opacity="0.1"/>
+<path d="M14.7071 59.2929L31.2929 42.7071C31.9229 42.0771 33 42.5233 33 43.4142V60C33 60.5523 32.5523 61 32 61H15.4142C14.5233 61 14.0771 59.9229 14.7071 59.2929Z" fill="white" stroke="#949AAB" stroke-width="1.25" stroke-linejoin="round"/>
+<path d="M19.75 54.75L21.8713 56.8713" stroke="#949AAB" stroke-width="1.25"/>
+<path d="M23.2856 51.2148L25.407 53.3362" stroke="#949AAB" stroke-width="1.25"/>
+<path d="M26.8213 47.6787L28.9426 49.8" stroke="#949AAB" stroke-width="1.25"/>
+</g>
+<defs>
+<clipPath id="clip0_8327_1165">
+<rect width="96" height="96" fill="white"/>
+</clipPath>
+</defs>
+</svg>