diff --git a/app/assets/javascripts/glql/components/common/facade.vue b/app/assets/javascripts/glql/components/common/facade.vue
index 964cd963a70986d00ad31c68aa141f113177bdd4..057ec6c5d2252fccdc950c8b2b60dade495e8195 100644
--- a/app/assets/javascripts/glql/components/common/facade.vue
+++ b/app/assets/javascripts/glql/components/common/facade.vue
@@ -31,7 +31,6 @@ export default {
   data() {
     return {
       loadOnClick: true,
-
       presenterPreview: null,
       presenterComponent: null,
       error: {
diff --git a/app/assets/javascripts/glql/components/presenters/issuable.vue b/app/assets/javascripts/glql/components/presenters/issuable.vue
index eb4791d00855c4d2ede46a31a37334a715e867b7..07b305c1ac44d09cdb4f65314a0cfbf8f439f678 100644
--- a/app/assets/javascripts/glql/components/presenters/issuable.vue
+++ b/app/assets/javascripts/glql/components/presenters/issuable.vue
@@ -46,6 +46,7 @@ export default {
 <template>
   <gl-link
     ref="reference"
+    class="gl-text-strong"
     :class="`gfm gfm-${type}`"
     :data-original="`${data.reference}+`"
     :data-reference-type="type"
diff --git a/app/assets/javascripts/glql/components/presenters/list.vue b/app/assets/javascripts/glql/components/presenters/list.vue
index 01e196a01424a0db7bff18eb6ae60000dd4f8d46..d0381b510d8399a6111088f9b948055d7c9f7251 100644
--- a/app/assets/javascripts/glql/components/presenters/list.vue
+++ b/app/assets/javascripts/glql/components/presenters/list.vue
@@ -2,6 +2,7 @@
 import { GlIcon, GlIntersperse, GlLink, GlSprintf, GlSkeletonLoader } from '@gitlab/ui';
 import { helpPagePath } from '~/helpers/help_page_helper';
 import { __ } from '~/locale';
+import CrudComponent from '~/vue_shared/components/crud_component.vue';
 
 export default {
   name: 'ListPresenter',
@@ -11,6 +12,7 @@ export default {
     GlLink,
     GlSprintf,
     GlSkeletonLoader,
+    CrudComponent,
   },
   inject: ['presenter'],
   props: {
@@ -37,11 +39,14 @@ export default {
     },
   },
   computed: {
+    title() {
+      return this.config.title || __('GLQL list');
+    },
     items() {
       return this.data.nodes || [];
     },
     fields() {
-      return this.config.fields;
+      return this.config.fields?.filter((item) => item.key !== 'title');
     },
     docsPath() {
       return `${helpPagePath('user/glql/_index')}#glql-views`;
@@ -53,8 +58,14 @@ export default {
 };
 </script>
 <template>
-  <div class="gl-mb-4">
-    <component :is="listType" class="!gl-mb-1" data-testid="list">
+  <crud-component
+    :title="title"
+    :description="config.description"
+    :count="items.length"
+    is-collapsible
+    class="!gl-mt-5"
+  >
+    <component :is="listType" class="content-list !gl-mb-0" data-testid="list">
       <template v-if="isPreview">
         <li v-for="i in 5" :key="i">
           <gl-skeleton-loader :width="400" :lines="1" />
@@ -64,8 +75,13 @@ export default {
         <li
           v-for="(item, itemIndex) in items"
           :key="itemIndex"
+          class="gl-py-3"
+          :class="{ 'gl-border-b gl-border-b-section': itemIndex !== items.length - 1 }"
           :data-testid="`list-item-${itemIndex}`"
         >
+          <h3 class="!gl-heading-5 !gl-mb-1">
+            <component :is="presenter.forField(item, 'title')" />
+          </h3>
           <gl-intersperse separator=" · ">
             <span v-for="field in fields" :key="field.key">
               <component :is="presenter.forField(item, field.key)" />
@@ -77,16 +93,16 @@ export default {
         {{ __('No data found for this query') }}
       </div>
     </component>
-    <div
-      class="gl-mt-3 gl-flex gl-items-center gl-gap-1 gl-text-sm gl-text-subtle"
-      data-testid="footer"
-    >
-      <gl-icon class="gl-mb-1 gl-mr-1" :size="12" name="tanuki" />
-      <gl-sprintf :message="$options.i18n.generatedMessage">
-        <template #link="{ content }">
-          <gl-link :href="docsPath" target="_blank">{{ content }}</gl-link>
-        </template>
-      </gl-sprintf>
-    </div>
-  </div>
+
+    <template #footer>
+      <div class="gl-flex gl-items-center gl-gap-1 gl-text-sm gl-text-subtle" data-testid="footer">
+        <gl-icon class="gl-mb-1 gl-mr-1" :size="12" name="tanuki" />
+        <gl-sprintf :message="$options.i18n.generatedMessage">
+          <template #link="{ content }">
+            <gl-link :href="docsPath" target="_blank">{{ content }}</gl-link>
+          </template>
+        </gl-sprintf>
+      </div>
+    </template>
+  </crud-component>
 </template>
diff --git a/app/assets/javascripts/glql/components/presenters/table.vue b/app/assets/javascripts/glql/components/presenters/table.vue
index 1a82e2df51e2ee2f86b54a8909074147bb27db88..81ba44b8814edc6a2ed67f5ae92251c678ee0f17 100644
--- a/app/assets/javascripts/glql/components/presenters/table.vue
+++ b/app/assets/javascripts/glql/components/presenters/table.vue
@@ -2,6 +2,7 @@
 import { GlIcon, GlLink, GlSprintf, GlSkeletonLoader } from '@gitlab/ui';
 import { helpPagePath } from '~/helpers/help_page_helper';
 import { __ } from '~/locale';
+import CrudComponent from '~/vue_shared/components/crud_component.vue';
 import Sorter from '../../core/sorter';
 import ThResizable from '../common/th_resizable.vue';
 
@@ -13,6 +14,7 @@ export default {
     GlSprintf,
     GlSkeletonLoader,
     ThResizable,
+    CrudComponent,
   },
   inject: ['presenter'],
   props: {
@@ -39,11 +41,13 @@ export default {
       items,
       fields: this.config.fields,
       sorter: new Sorter(items),
-
       table: null,
     };
   },
   computed: {
+    title() {
+      return this.config.title || __('GLQL table');
+    },
     docsPath() {
       return `${helpPagePath('user/glql/_index')}#glql-views`;
     },
@@ -59,61 +63,71 @@ export default {
 };
 </script>
 <template>
-  <div class="gl-table-shadow !gl-my-4">
-    <table ref="table" class="!gl-mb-2 !gl-mt-0 gl-overflow-y-hidden">
-      <thead>
-        <tr v-if="table">
-          <th-resizable v-for="(field, fieldIndex) in fields" :key="field.key" :table="table">
-            <div
-              :data-testid="`column-${fieldIndex}`"
-              class="gl-cursor-pointer"
-              @click="sorter.sortBy(field.key)"
-            >
-              {{ field.label }}
-              <gl-icon
-                v-if="sorter.options.fieldName === field.key"
-                :name="sorter.options.ascending ? 'arrow-up' : 'arrow-down'"
-              />
-            </div>
-          </th-resizable>
-        </tr>
-      </thead>
-      <tbody>
-        <template v-if="isPreview">
-          <tr v-for="i in 5" :key="i">
-            <td v-for="field in fields" :key="field.key">
-              <gl-skeleton-loader :width="120" :lines="1" />
-            </td>
+  <crud-component
+    :title="title"
+    :description="config.description"
+    :count="items.length"
+    is-collapsible
+    class="!gl-mt-5 gl-overflow-hidden"
+    body-class="!gl-m-[-1px] !gl-p-0"
+    footer-class="!gl-border-t-0"
+  >
+    <div class="gl-table-shadow">
+      <table ref="table" class="!gl-my-0 gl-overflow-y-hidden">
+        <thead class="gl-text-sm">
+          <tr v-if="table">
+            <th-resizable v-for="(field, fieldIndex) in fields" :key="field.key" :table="table">
+              <div
+                :data-testid="`column-${fieldIndex}`"
+                class="gl-cursor-pointer"
+                @click="sorter.sortBy(field.key)"
+              >
+                {{ field.label }}
+                <gl-icon
+                  v-if="sorter.options.fieldName === field.key"
+                  :name="sorter.options.ascending ? 'arrow-up' : 'arrow-down'"
+                />
+              </div>
+            </th-resizable>
           </tr>
-        </template>
-        <template v-else-if="items.length">
-          <tr
-            v-for="(item, itemIndex) in items"
-            :key="item.id"
-            :data-testid="`table-row-${itemIndex}`"
-          >
-            <td v-for="field in fields" :key="field.key">
-              <component :is="presenter.forField(item, field.key)" />
+        </thead>
+        <tbody class="!gl-bg-subtle">
+          <template v-if="isPreview">
+            <tr v-for="i in 5" :key="i">
+              <td v-for="field in fields" :key="field.key">
+                <gl-skeleton-loader :width="120" :lines="1" />
+              </td>
+            </tr>
+          </template>
+          <template v-else-if="items.length">
+            <tr
+              v-for="(item, itemIndex) in items"
+              :key="item.id"
+              :data-testid="`table-row-${itemIndex}`"
+            >
+              <td v-for="field in fields" :key="field.key">
+                <component :is="presenter.forField(item, field.key)" />
+              </td>
+            </tr>
+          </template>
+          <tr v-else-if="!items.length">
+            <td :colspan="fields.length" class="gl-text-center">
+              {{ __('No data found for this query') }}
             </td>
           </tr>
-        </template>
-        <tr v-else-if="!items.length">
-          <td :colspan="fields.length" class="gl-text-center">
-            {{ __('No data found for this query') }}
-          </td>
-        </tr>
-      </tbody>
-    </table>
-    <div
-      class="gl-mt-3 gl-flex gl-items-center gl-gap-1 gl-text-sm gl-text-subtle"
-      data-testid="footer"
-    >
-      <gl-icon class="gl-mb-1 gl-mr-1" :size="12" name="tanuki" />
-      <gl-sprintf :message="$options.i18n.generatedMessage">
-        <template #link="{ content }">
-          <gl-link :href="docsPath" target="_blank">{{ content }}</gl-link>
-        </template>
-      </gl-sprintf>
+        </tbody>
+      </table>
     </div>
-  </div>
+
+    <template #footer>
+      <div class="gl-flex gl-items-center gl-gap-1 gl-text-sm gl-text-subtle" data-testid="footer">
+        <gl-icon class="gl-mb-1 gl-mr-1" :size="12" name="tanuki" />
+        <gl-sprintf :message="$options.i18n.generatedMessage">
+          <template #link="{ content }">
+            <gl-link :href="docsPath" target="_blank">{{ content }}</gl-link>
+          </template>
+        </gl-sprintf>
+      </div>
+    </template>
+  </crud-component>
 </template>
diff --git a/app/assets/javascripts/glql/core/presenter.js b/app/assets/javascripts/glql/core/presenter.js
index e62e9b67ff7f026a24c4156c7a997a8c57849994..9969146f6fead5947429ec3318eca6f074bc4da6 100644
--- a/app/assets/javascripts/glql/core/presenter.js
+++ b/app/assets/javascripts/glql/core/presenter.js
@@ -38,7 +38,6 @@ const presentersByFieldName = {
 const presentersByDisplayType = {
   list: ListPresenter,
   orderedList: ListPresenter,
-
   table: TablePresenter,
 };
 
diff --git a/app/assets/javascripts/vue_shared/components/crud_component.vue b/app/assets/javascripts/vue_shared/components/crud_component.vue
index 2e83d1ca7a26beaa5fcb5535b0675b034c7192ea..e859c4bf1dc38275f4dbde34bbf06aad4ae04d5f 100644
--- a/app/assets/javascripts/vue_shared/components/crud_component.vue
+++ b/app/assets/javascripts/vue_shared/components/crud_component.vue
@@ -1,11 +1,12 @@
 <script>
-import { GlButton, GlIcon, GlLoadingIcon, GlLink, GlTooltipDirective } from '@gitlab/ui';
+import { GlButton, GlIcon, GlBadge, GlLoadingIcon, GlLink, GlTooltipDirective } from '@gitlab/ui';
 import { __ } from '~/locale';
 
 export default {
   components: {
     GlButton,
     GlIcon,
+    GlBadge,
     GlLoadingIcon,
     GlLink,
   },
@@ -83,6 +84,11 @@ export default {
       required: false,
       default: null,
     },
+    footerClass: {
+      type: [String, Object],
+      required: false,
+      default: null,
+    },
     persistCollapsedState: {
       type: Boolean,
       required: false,
@@ -208,14 +214,15 @@ export default {
           >
             <template v-if="displayedCount">
               <gl-icon v-if="icon" :name="icon" variant="subtle" data-testid="crud-icon" />
-              {{ displayedCount }}
+              <template v-if="icon">{{ displayedCount }}</template>
+              <gl-badge v-else class="gl-self-baseline">{{ displayedCount }}</gl-badge>
             </template>
             <slot v-if="$scopedSlots.count" name="count"></slot>
           </span>
         </h2>
         <p
           v-if="description || $scopedSlots.description"
-          class="gl-mb-0 gl-mt-2 gl-text-sm gl-leading-normal gl-text-subtle"
+          class="!gl-mb-0 !gl-mt-2 !gl-text-sm !gl-leading-normal !gl-text-subtle"
           data-testid="crud-description"
         >
           <slot v-if="$scopedSlots.description" name="description"></slot>
@@ -283,8 +290,9 @@ export default {
     </div>
 
     <footer
-      v-if="$scopedSlots.footer"
+      v-if="isContentVisible && $scopedSlots.footer"
       class="gl-border-t gl-rounded-b-base gl-border-section gl-bg-section gl-px-5 gl-py-4"
+      :class="footerClass"
       data-testid="crud-footer"
     >
       <slot name="footer"></slot>
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 601927366f742be81cef97d0a91e55f85fc8b27d..56e330a9bc0aa5dfc3a5aa7d7a42350bd45985d0 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -118,7 +118,7 @@
     @apply gl-heading-1-fixed gl-mt-7 gl-pb-2 gl-border-b;
   }
 
-  h2 {
+  h2:not(:where(.crud-header) h2) {
     @apply gl-heading-2-fixed gl-mt-6 gl-pb-2 gl-border-b;
   }
 
@@ -203,13 +203,24 @@
 
   .gl-table-shadow {
     overflow-x: auto;
+    overflow-y: hidden;
     animation: gl-table-shadow $gl-easing-out-cubic;
     animation-timeline: scroll(self inline);
+    // Needs to stay white because we use mix-blend-mode
+    // to make it work in both light and darkmode.
+    background-color: var(--gl-color-neutral-0);
 
     table:not(.code) {
       display: table;
       overflow-x: initial;
       mix-blend-mode: multiply;
+      // strech is not available yet, so we have to rely
+      // on the vendor prefixed ones below
+      min-width: strech;
+      /* stylelint-disable-next-line value-no-vendor-prefix */
+      min-width: -webkit-fill-available;
+      /* stylelint-disable-next-line value-no-vendor-prefix */
+      min-width: -moz-available;
     }
   }
 
diff --git a/doc/user/glql/_index.md b/doc/user/glql/_index.md
index c2792c88cc13d3f833bdd4b1221726a328873826..990ac22f3b8b7ad42bd6ad99c976eaed16d8e860 100644
--- a/doc/user/glql/_index.md
+++ b/doc/user/glql/_index.md
@@ -91,6 +91,7 @@ Values can include:
 {{< history >}}
 
 - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/508956) in GitLab 17.7: Configuring the presentation layer using YAML front matter is deprecated.
+- Parameters `title` and `description` [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/183709) in GitLab 17.10.
 
 {{< /history >}}
 
@@ -113,7 +114,7 @@ Views can be embedded in the following areas:
 The syntax of views is a superset of YAML that consists of:
 
 - The `query` parameter: Expressions joined together with a logical operator, such as `and`.
-- Parameters related to the presentation layer, like `display`, `limit`, or `fields`.
+- Parameters related to the presentation layer, like `display`, `limit`, or `fields`, `title`, and `description`.
 
 A GLQL view is defined in Markdown as a code block, similar to other code blocks like Mermaid.
 
@@ -125,6 +126,8 @@ For example:
 ````yaml
 ```glql
 display: table
+title: GLQL table 🎉
+description: This view lists my open issues
 fields: title, state, health, epic, milestone, weight, updated
 limit: 5
 query: project = "gitlab-org/gitlab" AND assignee = currentUser() AND state = opened
@@ -133,20 +136,22 @@ query: project = "gitlab-org/gitlab" AND assignee = currentUser() AND state = op
 
 This query should render a table like the one below:
 
-![A table listing issues assigned to the current user](img/glql_table_v17_8.png)
+![A table listing issues assigned to the current user](img/glql_table_v17_10.png)
 
 #### Presentation syntax
 
 Aside from the `query` parameter, you can configure presentation details for your GLQL query using some
-more parameters.
+more optional parameters.
 
 Supported parameters:
 
-| Parameter | Default | Description |
-| --------- | ------- | ----------- |
-| `display` | `table` | How to display the data. Supported options: `table`, `list`, or `orderedList`. |
-| `limit`   | `100`   | How many items to display. The maximum value is `100`. |
-| `fields`  | `title` | A comma-separated list of [fields](fields.md). All fields allowed in columns of a GLQL view are supported. |
+| Parameter     | Default                     | Description |
+| ------------- | --------------------------- | ----------- |
+| `description` | None                        | An optional description to display below the title. |
+| `display`     | `table`                     | How to display the data. Supported options: `table`, `list`, or `orderedList`. |
+| `fields`      | `title`                     | A comma-separated list of [fields](fields.md). All fields allowed in columns of a GLQL view are supported. |
+| `limit`       | `100`                       | How many items to display. The maximum value is `100`. |
+| `title`       | `GLQL table` or `GLQL list` | A title displayed at the top of the GLQL view. |
 
 For example, to display first five issues assigned to current user in the `gitlab-org/gitlab`
 project as a list, displaying fields `title`, `health`, and `due`:
diff --git a/doc/user/glql/img/glql_table_v17_10.png b/doc/user/glql/img/glql_table_v17_10.png
new file mode 100644
index 0000000000000000000000000000000000000000..b4023e32df4db769b5db96b7ded3ac9af498d1e4
Binary files /dev/null and b/doc/user/glql/img/glql_table_v17_10.png differ
diff --git a/doc/user/glql/img/glql_table_v17_8.png b/doc/user/glql/img/glql_table_v17_8.png
deleted file mode 100644
index ceb6b5ed3a6e962470af6e942b72468e2424c5ff..0000000000000000000000000000000000000000
Binary files a/doc/user/glql/img/glql_table_v17_8.png and /dev/null differ
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 1c648d7eff94def6a862a3b34583b3aee8f091e7..74bbe55f1985fd9e1c8a0419d7bcb10da1188b7c 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -25891,6 +25891,12 @@ msgstr ""
 msgid "GCP region configured"
 msgstr ""
 
+msgid "GLQL list"
+msgstr ""
+
+msgid "GLQL table"
+msgstr ""
+
 msgid "GLQL view timed out. Add more filters to reduce the number of results."
 msgstr ""
 
diff --git a/spec/frontend/glql/components/presenters/list_spec.js b/spec/frontend/glql/components/presenters/list_spec.js
index 2a107f103c3ffb24980f05d99386f4d29ab9f578..fd7925e23542ba9fa3de7a06cf3ad89421d99e83 100644
--- a/spec/frontend/glql/components/presenters/list_spec.js
+++ b/spec/frontend/glql/components/presenters/list_spec.js
@@ -51,9 +51,9 @@ describe('ListPresenter', () => {
     expect(htmlPresenter1.props('data')).toBe(MOCK_ISSUES.nodes[0].description);
     expect(htmlPresenter2.props('data')).toBe(MOCK_ISSUES.nodes[1].description);
 
-    expect(listItem1.text()).toEqual('Issue 1 (#1) · @foobar ·  Open · This is a description');
+    expect(listItem1.text()).toEqual('Issue 1 (#1) @foobar ·  Open · This is a description');
     expect(listItem2.text()).toEqual(
-      'Issue 2 (#2 - closed) · @janedoe ·  Closed · This is another description',
+      'Issue 2 (#2 - closed) @janedoe ·  Closed · This is another description',
     );
   });