diff --git a/app/assets/javascripts/discourse/app/components/basic-topic-list.hbs b/app/assets/javascripts/discourse/app/components/basic-topic-list.hbs
index 75124291958..67b00b92ab0 100644
--- a/app/assets/javascripts/discourse/app/components/basic-topic-list.hbs
+++ b/app/assets/javascripts/discourse/app/components/basic-topic-list.hbs
@@ -1,18 +1,34 @@
 <ConditionalLoadingSpinner @condition={{this.loading}}>
   {{#if this.topics}}
-    <TopicList
-      @showPosters={{this.showPosters}}
-      @hideCategory={{this.hideCategory}}
-      @topics={{this.topics}}
-      @expandExcerpts={{this.expandExcerpts}}
-      @bulkSelectHelper={{this.bulkSelectHelper}}
-      @canBulkSelect={{this.canBulkSelect}}
-      @tagsForUser={{this.tagsForUser}}
-      @changeSort={{this.changeSort}}
-      @order={{this.order}}
-      @ascending={{this.ascending}}
-      @focusLastVisitedTopic={{this.focusLastVisitedTopic}}
-    />
+    {{#if this.currentUser.use_glimmer_topic_list}}
+      <TopicList::List
+        @showPosters={{this.showPosters}}
+        @hideCategory={{this.hideCategory}}
+        @topics={{this.topics}}
+        @expandExcerpts={{this.expandExcerpts}}
+        @bulkSelectHelper={{this.bulkSelectHelper}}
+        @canBulkSelect={{this.canBulkSelect}}
+        @tagsForUser={{this.tagsForUser}}
+        @changeSort={{this.changeSort}}
+        @order={{this.order}}
+        @ascending={{this.ascending}}
+        @focusLastVisitedTopic={{this.focusLastVisitedTopic}}
+      />
+    {{else}}
+      <TopicList
+        @showPosters={{this.showPosters}}
+        @hideCategory={{this.hideCategory}}
+        @topics={{this.topics}}
+        @expandExcerpts={{this.expandExcerpts}}
+        @bulkSelectHelper={{this.bulkSelectHelper}}
+        @canBulkSelect={{this.canBulkSelect}}
+        @tagsForUser={{this.tagsForUser}}
+        @changeSort={{this.changeSort}}
+        @order={{this.order}}
+        @ascending={{this.ascending}}
+        @focusLastVisitedTopic={{this.focusLastVisitedTopic}}
+      />
+    {{/if}}
   {{else}}
     {{#unless this.loadingMore}}
       <div class="alert alert-info">
diff --git a/app/assets/javascripts/discourse/app/components/categories-topic-list.hbs b/app/assets/javascripts/discourse/app/components/categories-topic-list.hbs
index 397ab266a1a..6f424eb798b 100644
--- a/app/assets/javascripts/discourse/app/components/categories-topic-list.hbs
+++ b/app/assets/javascripts/discourse/app/components/categories-topic-list.hbs
@@ -8,8 +8,13 @@
 
 {{#if this.topics}}
   {{#each this.topics as |t|}}
-    <LatestTopicListItem @topic={{t}} />
+    {{#if this.currentUser.use_glimmer_topic_list}}
+      <TopicList::LatestTopicListItem @topic={{t}} />
+    {{else}}
+      <LatestTopicListItem @topic={{t}} />
+    {{/if}}
   {{/each}}
+
   <div class="more-topics">
     {{#if
       (eq
diff --git a/app/assets/javascripts/discourse/app/components/discovery/topics.hbs b/app/assets/javascripts/discourse/app/components/discovery/topics.hbs
index 9fd91eb2046..2d42462c8d4 100644
--- a/app/assets/javascripts/discourse/app/components/discovery/topics.hbs
+++ b/app/assets/javascripts/discourse/app/components/discovery/topics.hbs
@@ -7,15 +7,27 @@
 {{/if}}
 
 {{#if @model.sharedDrafts}}
-  <TopicList
-    @listTitle="shared_drafts.title"
-    @top={{this.top}}
-    @hideCategory="true"
-    @category={{@category}}
-    @topics={{@model.sharedDrafts}}
-    @discoveryList={{true}}
-    class="shared-drafts"
-  />
+  {{#if this.currentUser.use_glimmer_topic_list}}
+    <TopicList::List
+      @listTitle="shared_drafts.title"
+      @top={{this.top}}
+      @hideCategory="true"
+      @category={{@category}}
+      @topics={{@model.sharedDrafts}}
+      @discoveryList={{true}}
+      class="shared-drafts"
+    />
+  {{else}}
+    <TopicList
+      @listTitle="shared_drafts.title"
+      @top={{this.top}}
+      @hideCategory="true"
+      @category={{@category}}
+      @topics={{@model.sharedDrafts}}
+      @discoveryList={{true}}
+      class="shared-drafts"
+    />
+  {{/if}}
 {{/if}}
 
 <DiscoveryTopicsList
@@ -75,31 +87,59 @@
   </span>
 
   {{#if this.hasTopics}}
-    <TopicList
-      @highlightLastVisited={{true}}
-      @top={{this.top}}
-      @hot={{this.hot}}
-      @showTopicPostBadges={{this.showTopicPostBadges}}
-      @showPosters={{true}}
-      @canBulkSelect={{@canBulkSelect}}
-      @bulkSelectHelper={{@bulkSelectHelper}}
-      @changeSort={{@changeSort}}
-      @hideCategory={{@model.hideCategory}}
-      @order={{this.order}}
-      @ascending={{this.ascending}}
-      @expandGloballyPinned={{this.expandGloballyPinned}}
-      @expandAllPinned={{this.expandAllPinned}}
-      @category={{@category}}
-      @topics={{@model.topics}}
-      @discoveryList={{true}}
-      @focusLastVisitedTopic={{true}}
-      @showTopicsAndRepliesToggle={{this.showTopicsAndRepliesToggle}}
-      @newListSubset={{@model.params.subset}}
-      @changeNewListSubset={{@changeNewListSubset}}
-      @newRepliesCount={{this.newRepliesCount}}
-      @newTopicsCount={{this.newTopicsCount}}
-    />
+    {{#if this.currentUser.use_glimmer_topic_list}}
+      <TopicList::List
+        @highlightLastVisited={{true}}
+        @top={{this.top}}
+        @hot={{this.hot}}
+        @showTopicPostBadges={{this.showTopicPostBadges}}
+        @showPosters={{true}}
+        @canBulkSelect={{@canBulkSelect}}
+        @bulkSelectHelper={{@bulkSelectHelper}}
+        @changeSort={{@changeSort}}
+        @hideCategory={{@model.hideCategory}}
+        @order={{this.order}}
+        @ascending={{this.ascending}}
+        @expandGloballyPinned={{this.expandGloballyPinned}}
+        @expandAllPinned={{this.expandAllPinned}}
+        @category={{@category}}
+        @topics={{@model.topics}}
+        @discoveryList={{true}}
+        @focusLastVisitedTopic={{true}}
+        @showTopicsAndRepliesToggle={{this.showTopicsAndRepliesToggle}}
+        @newListSubset={{@model.params.subset}}
+        @changeNewListSubset={{@changeNewListSubset}}
+        @newRepliesCount={{this.newRepliesCount}}
+        @newTopicsCount={{this.newTopicsCount}}
+      />
+    {{else}}
+      <TopicList
+        @highlightLastVisited={{true}}
+        @top={{this.top}}
+        @hot={{this.hot}}
+        @showTopicPostBadges={{this.showTopicPostBadges}}
+        @showPosters={{true}}
+        @canBulkSelect={{@canBulkSelect}}
+        @bulkSelectHelper={{@bulkSelectHelper}}
+        @changeSort={{@changeSort}}
+        @hideCategory={{@model.hideCategory}}
+        @order={{this.order}}
+        @ascending={{this.ascending}}
+        @expandGloballyPinned={{this.expandGloballyPinned}}
+        @expandAllPinned={{this.expandAllPinned}}
+        @category={{@category}}
+        @topics={{@model.topics}}
+        @discoveryList={{true}}
+        @focusLastVisitedTopic={{true}}
+        @showTopicsAndRepliesToggle={{this.showTopicsAndRepliesToggle}}
+        @newListSubset={{@model.params.subset}}
+        @changeNewListSubset={{@changeNewListSubset}}
+        @newRepliesCount={{this.newRepliesCount}}
+        @newTopicsCount={{this.newTopicsCount}}
+      />
+    {{/if}}
   {{/if}}
+
   <span>
     <PluginOutlet
       @name="after-topic-list"
diff --git a/app/assets/javascripts/discourse/app/components/new-list-header-controls-wrapper.gjs b/app/assets/javascripts/discourse/app/components/new-list-header-controls-wrapper.gjs
index 1036e920144..792e8169fb5 100644
--- a/app/assets/javascripts/discourse/app/components/new-list-header-controls-wrapper.gjs
+++ b/app/assets/javascripts/discourse/app/components/new-list-header-controls-wrapper.gjs
@@ -1,9 +1,13 @@
 import Component from "@glimmer/component";
 import { on } from "@ember/modifier";
 import { action } from "@ember/object";
+import { service } from "@ember/service";
+import NewListHeaderControls from "discourse/components/topic-list/new-list-header-controls";
 import raw from "discourse/helpers/raw";
 
 export default class NewListHeaderControlsWrapper extends Component {
+  @service currentUser;
+
   @action
   click(e) {
     const target = e.target;
@@ -17,18 +21,30 @@ export default class NewListHeaderControlsWrapper extends Component {
   }
 
   <template>
-    <div
-      {{! template-lint-disable no-invalid-interactive }}
-      {{on "click" this.click}}
-      class="topic-replies-toggle-wrapper"
-    >
-      {{raw
-        "list/new-list-header-controls"
-        current=@current
-        newRepliesCount=@newRepliesCount
-        newTopicsCount=@newTopicsCount
-        noStaticLabel=true
-      }}
-    </div>
+    {{#if this.currentUser.use_glimmer_topic_list}}
+      <div class="topic-replies-toggle-wrapper">
+        <NewListHeaderControls
+          @current={{@current}}
+          @newRepliesCount={{@newRepliesCount}}
+          @newTopicsCount={{@newTopicsCount}}
+          @noStaticLabel={{true}}
+          @changeNewListSubset={{@changeNewListSubset}}
+        />
+      </div>
+    {{else}}
+      <div
+        {{! template-lint-disable no-invalid-interactive }}
+        {{on "click" this.click}}
+        class="topic-replies-toggle-wrapper"
+      >
+        {{raw
+          "list/new-list-header-controls"
+          current=@current
+          newRepliesCount=@newRepliesCount
+          newTopicsCount=@newTopicsCount
+          noStaticLabel=true
+        }}
+      </div>
+    {{/if}}
   </template>
 }
diff --git a/app/assets/javascripts/discourse/app/components/parent-category-row.hbs b/app/assets/javascripts/discourse/app/components/parent-category-row.hbs
index 564e83cca7c..83e3f2b06a9 100644
--- a/app/assets/javascripts/discourse/app/components/parent-category-row.hbs
+++ b/app/assets/javascripts/discourse/app/components/parent-category-row.hbs
@@ -71,7 +71,11 @@
       {{#if this.showTopics}}
         <td class="latest">
           {{#each this.category.featuredTopics as |t|}}
-            <FeaturedTopic @topic={{t}} />
+            {{#if this.currentUser.use_glimmer_topic_list}}
+              <TopicList::FeaturedTopic @topic={{t}} />
+            {{else}}
+              <FeaturedTopic @topic={{t}} />
+            {{/if}}
           {{/each}}
         </td>
       {{/if}}
diff --git a/app/assets/javascripts/discourse/app/components/topic-list.js b/app/assets/javascripts/discourse/app/components/topic-list.js
index 9c7c841b86a..1f5432c1f27 100644
--- a/app/assets/javascripts/discourse/app/components/topic-list.js
+++ b/app/assets/javascripts/discourse/app/components/topic-list.js
@@ -10,12 +10,14 @@ import TopicBulkActions from "./modal/topic-bulk-actions";
 export default Component.extend(LoadMore, {
   modal: service(),
   router: service(),
+  siteSettings: service(),
 
   tagName: "table",
   classNames: ["topic-list"],
   classNameBindings: ["bulkSelectEnabled:sticky-header"],
   showTopicPostBadges: true,
   listTitle: "topic.title",
+  lastCheckedElementId: null,
 
   get canDoBulkActions() {
     return (
diff --git a/app/assets/javascripts/discourse/app/components/topic-list/action-list.gjs b/app/assets/javascripts/discourse/app/components/topic-list/action-list.gjs
new file mode 100644
index 00000000000..1cfb2225622
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/topic-list/action-list.gjs
@@ -0,0 +1,14 @@
+import icon from "discourse-common/helpers/d-icon";
+
+const ActionList = <template>
+  {{#if @postNumbers}}
+    <div class="post-actions" ...attributes>
+      {{icon @icon}}
+      {{#each @postNumbers as |postNumber|}}
+        <a href="{{@topic.url}}/{{postNumber}}">#{{postNumber}}</a>
+      {{/each}}
+    </div>
+  {{/if}}
+</template>;
+
+export default ActionList;
diff --git a/app/assets/javascripts/discourse/app/components/topic-list/activity-column.gjs b/app/assets/javascripts/discourse/app/components/topic-list/activity-column.gjs
new file mode 100644
index 00000000000..aa7ecf772c9
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/topic-list/activity-column.gjs
@@ -0,0 +1,37 @@
+import Component from "@glimmer/component";
+import { service } from "@ember/service";
+import { htmlSafe } from "@ember/template";
+import PluginOutlet from "discourse/components/plugin-outlet";
+import coldAgeClass from "discourse/helpers/cold-age-class";
+import concatClass from "discourse/helpers/concat-class";
+import element from "discourse/helpers/element";
+import formatDate from "discourse/helpers/format-date";
+
+export default class ActivityColumn extends Component {
+  @service siteSettings;
+
+  get wrapperElement() {
+    return element(this.args.tagName ?? "td");
+  }
+
+  <template>
+    <this.wrapperElement
+      title={{htmlSafe @topic.bumpedAtTitle}}
+      class={{concatClass
+        "activity"
+        (coldAgeClass @topic.createdAt startDate=@topic.bumpedAt class="")
+      }}
+      ...attributes
+    >
+      <a
+        href={{@topic.lastPostUrl}}
+        class="post-activity"
+      >{{! no whitespace
+        }}<PluginOutlet
+          @name="topic-list-before-relative-date"
+        />
+        {{~formatDate @topic.bumpedAt format="tiny" noTitle="true"~}}
+      </a>
+    </this.wrapperElement>
+  </template>
+}
diff --git a/app/assets/javascripts/discourse/app/components/topic-list/featured-topic.gjs b/app/assets/javascripts/discourse/app/components/topic-list/featured-topic.gjs
new file mode 100644
index 00000000000..d5ba17857cd
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/topic-list/featured-topic.gjs
@@ -0,0 +1,43 @@
+import { on } from "@ember/modifier";
+import { htmlSafe } from "@ember/template";
+import TopicEntrance from "discourse/components/topic-list/topic-entrance";
+import TopicPostBadges from "discourse/components/topic-post-badges";
+import TopicStatus from "discourse/components/topic-status";
+import formatAge from "discourse/helpers/format-age";
+import { modKeysPressed } from "discourse/lib/utilities";
+
+const onTimestampClick = function (event) {
+  if (modKeysPressed(event).length) {
+    // Allow opening the link in a new tab/window
+    event.stopPropagation();
+  } else {
+    // Otherwise only display the TopicEntrance component
+    event.preventDefault();
+  }
+};
+
+const FeaturedTopic = <template>
+  <div data-topic-id={{@topic.id}} class="featured-topic --glimmer">
+    <TopicStatus @topic={{@topic}} />
+
+    <a href={{@topic.lastUnreadUrl}} class="title">{{htmlSafe
+        @topic.fancyTitle
+      }}</a>
+
+    <TopicPostBadges
+      @unreadPosts={{@topic.unread_posts}}
+      @unseen={{@topic.unseen}}
+      @url={{@topic.lastUnreadUrl}}
+    />
+
+    <TopicEntrance @topic={{@topic}}>
+      <a
+        {{on "click" onTimestampClick}}
+        href={{@topic.lastPostUrl}}
+        class="last-posted-at"
+      >{{formatAge @topic.last_posted_at}}</a>
+    </TopicEntrance>
+  </div>
+</template>;
+
+export default FeaturedTopic;
diff --git a/app/assets/javascripts/discourse/app/components/topic-list/latest-topic-list-item.gjs b/app/assets/javascripts/discourse/app/components/topic-list/latest-topic-list-item.gjs
new file mode 100644
index 00000000000..c940bd0a958
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/topic-list/latest-topic-list-item.gjs
@@ -0,0 +1,94 @@
+import Component from "@glimmer/component";
+import { concat, hash } from "@ember/helper";
+import { service } from "@ember/service";
+import PluginOutlet from "discourse/components/plugin-outlet";
+import PostsCountColumn from "discourse/components/topic-list/posts-count-column";
+import TopicPostBadges from "discourse/components/topic-post-badges";
+import TopicStatus from "discourse/components/topic-status";
+import UserAvatarFlair from "discourse/components/user-avatar-flair";
+import UserLink from "discourse/components/user-link";
+import avatar from "discourse/helpers/avatar";
+import categoryLink from "discourse/helpers/category-link";
+import concatClass from "discourse/helpers/concat-class";
+import discourseTags from "discourse/helpers/discourse-tags";
+import formatDate from "discourse/helpers/format-date";
+import topicFeaturedLink from "discourse/helpers/topic-featured-link";
+import topicLink from "discourse/helpers/topic-link";
+
+export default class LatestTopicListItem extends Component {
+  @service appEvents;
+
+  get tagClassNames() {
+    if (this.args.topic.tags) {
+      return this.args.topic.tags.map((tagName) => `tag-${tagName}`);
+    }
+  }
+
+  <template>
+    <div
+      data-topic-id={{@topic.id}}
+      class={{concatClass
+        "latest-topic-list-item"
+        this.tagClassNames
+        (if @topic.category (concat "category-" @topic.category.fullSlug))
+        (if @topic.liked "liked")
+        (if @topic.archived "archived")
+        (if @topic.bookmarked "bookmarked")
+        (if @topic.pinned "pinned")
+        (if @topic.closed "closed")
+        (if @topic.visited "visited")
+      }}
+    >
+      <PluginOutlet
+        @name="above-latest-topic-list-item"
+        @connectorTagName="div"
+        @outletArgs={{hash topic=@topic}}
+      />
+
+      <div class="topic-poster">
+        <UserLink @user={{@topic.lastPosterUser}}>
+          {{avatar @topic.lastPosterUser imageSize="large"}}
+        </UserLink>
+        <UserAvatarFlair @user={{@topic.lastPosterUser}} />
+      </div>
+
+      <div class="main-link">
+        <div class="top-row">
+          <TopicStatus @topic={{@topic}} />
+
+          {{topicLink @topic}}
+          {{~#if @topic.featured_link}}
+            &nbsp;{{topicFeaturedLink @topic}}
+          {{/if~}}
+          <TopicPostBadges
+            @unreadPosts={{@topic.unread_posts}}
+            @unseen={{@topic.unseen}}
+            @url={{@topic.lastUnreadUrl}}
+          />
+        </div>
+
+        <div class="bottom-row">
+          {{categoryLink @topic.category~}}
+          {{~discourseTags @topic mode="list"}}
+        </div>
+      </div>
+
+      <div class="topic-stats">
+        <PluginOutlet
+          @name="above-latest-topic-list-item-post-count"
+          @connectorTagName="div"
+          @outletArgs={{hash topic=@topic}}
+        />
+
+        <PostsCountColumn @topic={{@topic}} @tagName="div" />
+
+        <div class="topic-last-activity">
+          <a
+            href={{@topic.lastPostUrl}}
+            title={{@topic.bumpedAtTitle}}
+          >{{formatDate @topic.bumpedAt format="tiny" noTitle="true"}}</a>
+        </div>
+      </div>
+    </div>
+  </template>
+}
diff --git a/app/assets/javascripts/discourse/app/components/topic-list/list.gjs b/app/assets/javascripts/discourse/app/components/topic-list/list.gjs
new file mode 100644
index 00000000000..69e595db553
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/topic-list/list.gjs
@@ -0,0 +1,190 @@
+import Component from "@glimmer/component";
+import { tracked } from "@glimmer/tracking";
+import { fn, hash } from "@ember/helper";
+import { service } from "@ember/service";
+import { eq, or } from "truth-helpers";
+import PluginOutlet from "discourse/components/plugin-outlet";
+import TopicListHeader from "discourse/components/topic-list/topic-list-header";
+import TopicListItem from "discourse/components/topic-list/topic-list-item";
+import concatClass from "discourse/helpers/concat-class";
+import i18n from "discourse-common/helpers/i18n";
+
+export default class TopicList extends Component {
+  @service currentUser;
+  @service router;
+  @service siteSettings;
+
+  @tracked lastCheckedElementId;
+
+  get selected() {
+    return this.args.bulkSelectHelper?.selected;
+  }
+
+  get bulkSelectEnabled() {
+    return this.args.bulkSelectHelper?.bulkSelectEnabled;
+  }
+
+  get canDoBulkActions() {
+    return this.currentUser?.canManageTopic && this.selected?.length;
+  }
+
+  get toggleInTitle() {
+    return !this.bulkSelectEnabled && this.args.canBulkSelect;
+  }
+
+  get experimentalTopicBulkActionsEnabled() {
+    return this.currentUser?.use_experimental_topic_bulk_actions;
+  }
+
+  get sortable() {
+    return !!this.args.changeSort;
+  }
+
+  get showLikes() {
+    return this.args.order === "likes";
+  }
+
+  get showOpLikes() {
+    return this.args.order === "op_likes";
+  }
+
+  get lastVisitedTopic() {
+    const { topics, order, ascending, top, hot } = this.args;
+
+    if (
+      !this.args.highlightLastVisited ||
+      top ||
+      hot ||
+      ascending ||
+      !topics ||
+      topics.length === 1 ||
+      (order && order !== "activity") ||
+      !this.currentUser?.get("previous_visit_at")
+    ) {
+      return;
+    }
+
+    // work backwards
+    // this is more efficient cause we keep appending to list
+    const start = topics.findIndex((topic) => !topic.get("pinned"));
+    let lastVisitedTopic, topic;
+
+    for (let i = topics.length - 1; i >= start; i--) {
+      if (topics[i].get("bumpedAt") > this.currentUser.get("previousVisitAt")) {
+        lastVisitedTopic = topics[i];
+        break;
+      }
+      topic = topics[i];
+    }
+
+    if (!lastVisitedTopic || !topic) {
+      return;
+    }
+
+    // end of list that was scanned
+    if (topic.get("bumpedAt") > this.currentUser.get("previousVisitAt")) {
+      return;
+    }
+
+    return lastVisitedTopic;
+  }
+
+  <template>
+    {{! template-lint-disable table-groups }}
+    <table
+      class={{concatClass
+        "topic-list"
+        (if this.bulkSelectEnabled "sticky-header")
+      }}
+    >
+      <thead class="topic-list-header">
+        <TopicListHeader
+          @canBulkSelect={{@canBulkSelect}}
+          @toggleInTitle={{this.toggleInTitle}}
+          @category={{@category}}
+          @hideCategory={{@hideCategory}}
+          @showPosters={{@showPosters}}
+          @showLikes={{this.showLikes}}
+          @showOpLikes={{this.showOpLikes}}
+          @order={{@order}}
+          @changeSort={{@changeSort}}
+          @ascending={{@ascending}}
+          @sortable={{this.sortable}}
+          @listTitle={{or @listTitle "topic.title"}}
+          @bulkSelectEnabled={{this.bulkSelectEnabled}}
+          @bulkSelectHelper={{@bulkSelectHelper}}
+          @experimentalTopicBulkActionsEnabled={{this.experimentalTopicBulkActionsEnabled}}
+          @canDoBulkActions={{this.canDoBulkActions}}
+          @showTopicsAndRepliesToggle={{@showTopicsAndRepliesToggle}}
+          @newListSubset={{@newListSubset}}
+          @newRepliesCount={{@newRepliesCount}}
+          @newTopicsCount={{@newTopicsCount}}
+          @changeNewListSubset={{@changeNewListSubset}}
+        />
+      </thead>
+
+      <PluginOutlet
+        @name="before-topic-list-body"
+        @outletArgs={{hash
+          topics=@topics
+          selected=this.selected
+          bulkSelectEnabled=this.bulkSelectEnabled
+          lastVisitedTopic=this.lastVisitedTopic
+          discoveryList=@discoveryList
+          hideCategory=@hideCategory
+        }}
+      />
+
+      <tbody class="topic-list-body">
+        {{#each @topics as |topic index|}}
+          <TopicListItem
+            @topic={{topic}}
+            @bulkSelectEnabled={{this.bulkSelectEnabled}}
+            @showTopicPostBadges={{@showTopicPostBadges}}
+            @hideCategory={{@hideCategory}}
+            @showPosters={{@showPosters}}
+            @showLikes={{this.showLikes}}
+            @showOpLikes={{this.showOpLikes}}
+            @expandGloballyPinned={{@expandGloballyPinned}}
+            @expandAllPinned={{@expandAllPinned}}
+            @lastVisitedTopic={{this.lastVisitedTopic}}
+            @selected={{this.selected}}
+            @lastCheckedElementId={{this.lastCheckedElementId}}
+            @updateLastCheckedElementId={{fn (mut this.lastCheckedElementId)}}
+            @tagsForUser={{@tagsForUser}}
+            @focusLastVisitedTopic={{@focusLastVisitedTopic}}
+            @index={{index}}
+          />
+
+          {{#if (eq topic this.lastVisitedTopic)}}
+            <tr class="topic-list-item-separator">
+              <td class="topic-list-data" colspan="6">
+                <span>
+                  {{i18n "topics.new_messages_marker"}}
+                </span>
+              </td>
+            </tr>
+          {{/if}}
+
+          <PluginOutlet
+            @name="after-topic-list-item"
+            @outletArgs={{hash topic=topic index=index}}
+            @connectorTagName="tr"
+          />
+        {{/each}}
+      </tbody>
+
+      <PluginOutlet
+        @name="after-topic-list-body"
+        @outletArgs={{hash
+          topics=@topics
+          selected=this.selected
+          bulkSelectEnabled=this.bulkSelectEnabled
+          lastVisitedTopic=this.lastVisitedTopic
+          discoveryList=@discoveryList
+          hideCategory=@hideCategory
+        }}
+      />
+    </table>
+  </template>
+}
diff --git a/app/assets/javascripts/discourse/app/components/topic-list/new-list-header-controls.gjs b/app/assets/javascripts/discourse/app/components/topic-list/new-list-header-controls.gjs
new file mode 100644
index 00000000000..a60e6907b89
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/topic-list/new-list-header-controls.gjs
@@ -0,0 +1,90 @@
+import Component from "@glimmer/component";
+import { fn } from "@ember/helper";
+import { on } from "@ember/modifier";
+import concatClass from "discourse/helpers/concat-class";
+import i18n from "discourse-common/helpers/i18n";
+
+export default class NewListHeaderControls extends Component {
+  get topicsActive() {
+    return this.args.current === "topics";
+  }
+
+  get repliesActive() {
+    return this.args.current === "replies";
+  }
+
+  get allActive() {
+    return !this.topicsActive && !this.repliesActive;
+  }
+
+  get repliesButtonLabel() {
+    if (this.args.newRepliesCount > 0) {
+      return i18n("filters.new.replies_with_count", {
+        count: this.args.newRepliesCount,
+      });
+    } else {
+      return i18n("filters.new.replies");
+    }
+  }
+
+  get topicsButtonLabel() {
+    if (this.args.newTopicsCount > 0) {
+      return i18n("filters.new.topics_with_count", {
+        count: this.args.newTopicsCount,
+      });
+    } else {
+      return i18n("filters.new.topics");
+    }
+  }
+
+  get staticLabel() {
+    if (
+      this.args.noStaticLabel ||
+      (this.args.newTopicsCount > 0 && this.args.newRepliesCount > 0)
+    ) {
+      return;
+    }
+
+    if (this.args.newTopicsCount > 0) {
+      return this.topicsButtonLabel;
+    } else {
+      return this.repliesButtonLabel;
+    }
+  }
+
+  <template>
+    {{#if this.staticLabel}}
+      <span class="static-label">{{this.staticLabel}}</span>
+    {{else}}
+      <button
+        {{on "click" (fn @changeNewListSubset null)}}
+        class={{concatClass
+          "topics-replies-toggle --all"
+          (if this.allActive "active")
+        }}
+      >
+        {{i18n "filters.new.all"}}
+      </button>
+
+      <button
+        {{on "click" (fn @changeNewListSubset "topics")}}
+        class={{concatClass
+          "topics-replies-toggle --topics"
+          (if this.topicsActive "active")
+        }}
+      >
+        {{this.topicsButtonLabel}}
+      </button>
+
+      <button
+        {{on "click" (fn @changeNewListSubset "replies")}}
+        class={{concatClass
+          "topics-replies-toggle --replies"
+          (if this.repliesActive "active")
+        }}
+      >
+        {{this.repliesButtonLabel}}
+      </button>
+    {{/if}}
+  </template>
+}
diff --git a/app/assets/javascripts/discourse/app/components/topic-list/participant-groups.gjs b/app/assets/javascripts/discourse/app/components/topic-list/participant-groups.gjs
new file mode 100644
index 00000000000..1d25593a868
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/topic-list/participant-groups.gjs
@@ -0,0 +1,25 @@
+import icon from "discourse-common/helpers/d-icon";
+import i18n from "discourse-common/helpers/i18n";
+
+const ParticipantGroups = <template>
+  <div
+    role="list"
+    aria-label={{i18n "topic.participant_groups"}}
+    class="participant-group-wrapper"
+  >
+    {{#each @groups as |group|}}
+      <div class="participant-group">
+        <a
+          href={{group.url}}
+          data-group-card={{group.name}}
+          class="user-group trigger-group-card"
+        >
+          {{icon "users"}}
+          {{group.name}}
+        </a>
+      </div>
+    {{/each}}
+  </div>
+</template>;
+
+export default ParticipantGroups;
diff --git a/app/assets/javascripts/discourse/app/components/topic-list/post-count-or-badges.gjs b/app/assets/javascripts/discourse/app/components/topic-list/post-count-or-badges.gjs
new file mode 100644
index 00000000000..6a5c265fd91
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/topic-list/post-count-or-badges.gjs
@@ -0,0 +1,17 @@
+import { and } from "truth-helpers";
+import PostsCountColumn from "discourse/components/topic-list/posts-count-column";
+import TopicPostBadges from "discourse/components/topic-post-badges";
+
+const PostCountOrBadges = <template>
+  {{#if (and @postBadgesEnabled @topic.unread_posts)}}
+    <TopicPostBadges
+      @unreadPosts={{@topic.unread_posts}}
+      @unseen={{@topic.unseen}}
+      @url={{@topic.lastUnreadUrl}}
+    />
+  {{else}}
+    <PostsCountColumn @topic={{@topic}} @tagName="div" />
+  {{/if}}
+</template>;
+
+export default PostCountOrBadges;
diff --git a/app/assets/javascripts/discourse/app/components/topic-list/posters-column.gjs b/app/assets/javascripts/discourse/app/components/topic-list/posters-column.gjs
new file mode 100644
index 00000000000..7b0d42b195e
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/topic-list/posters-column.gjs
@@ -0,0 +1,25 @@
+import avatar from "discourse/helpers/avatar";
+
+const PostersColumn = <template>
+  <td class="posters topic-list-data">
+    {{#each @posters as |poster|}}
+      {{#if poster.moreCount}}
+        <a class="posters-more-count">{{poster.moreCount}}</a>
+      {{else}}
+        <a
+          href={{poster.user.path}}
+          data-user-card={{poster.user.username}}
+          class={{poster.extraClasses}}
+        >{{avatar
+            poster
+            avatarTemplatePath="user.avatar_template"
+            usernamePath="user.username"
+            namePath="user.name"
+            imageSize="small"
+          }}</a>
+      {{/if}}
+    {{/each}}
+  </td>
+</template>;
+
+export default PostersColumn;
diff --git a/app/assets/javascripts/discourse/app/components/topic-list/posts-count-column.gjs b/app/assets/javascripts/discourse/app/components/topic-list/posts-count-column.gjs
new file mode 100644
index 00000000000..70fa158f208
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/topic-list/posts-count-column.gjs
@@ -0,0 +1,67 @@
+import Component from "@glimmer/component";
+import { service } from "@ember/service";
+import PluginOutlet from "discourse/components/plugin-outlet";
+import TopicEntrance from "discourse/components/topic-list/topic-entrance";
+import element from "discourse/helpers/element";
+import number from "discourse/helpers/number";
+import I18n from "discourse-i18n";
+
+export default class PostsCountColumn extends Component {
+  @service siteSettings;
+
+  get ratio() {
+    const likes = parseFloat(this.args.topic.like_count);
+    const posts = parseFloat(this.args.topic.posts_count);
+
+    if (posts < 10) {
+      return 0;
+    }
+
+    return (likes || 0) / posts;
+  }
+
+  get title() {
+    return I18n.messageFormat("posts_likes_MF", {
+      count: this.args.topic.replyCount,
+      ratio: this.ratioText,
+    }).trim();
+  }
+
+  get ratioText() {
+    if (this.ratio > this.siteSettings.topic_post_like_heat_high) {
+      return "high";
+    }
+    if (this.ratio > this.siteSettings.topic_post_like_heat_medium) {
+      return "med";
+    }
+    if (this.ratio > this.siteSettings.topic_post_like_heat_low) {
+      return "low";
+    }
+    return "";
+  }
+
+  get likesHeat() {
+    if (this.ratioText?.length) {
+      return `heatmap-${this.ratioText}`;
+    }
+  }
+
+  get wrapperElement() {
+    return element(this.args.tagName ?? "td");
+  }
+
+  <template>
+    <this.wrapperElement
+      class="num posts-map posts {{this.likesHeat}} topic-list-data"
+    >
+      <TopicEntrance
+        @topic={{@topic}}
+        @title={{this.title}}
+        @triggerClass="btn-link posts-map badge-posts {{this.likesHeat}}"
+      >
+        <PluginOutlet @name="topic-list-before-reply-count" />
+        {{number @topic.replyCount noTitle="true"}}
+      </TopicEntrance>
+    </this.wrapperElement>
+  </template>
+}
diff --git a/app/assets/javascripts/discourse/app/components/topic-list/topic-bulk-select-dropdown.gjs b/app/assets/javascripts/discourse/app/components/topic-list/topic-bulk-select-dropdown.gjs
new file mode 100644
index 00000000000..735256c7088
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/topic-list/topic-bulk-select-dropdown.gjs
@@ -0,0 +1,16 @@
+import BulkSelectTopicsDropdown from "discourse/components/bulk-select-topics-dropdown";
+import i18n from "discourse-common/helpers/i18n";
+
+const TopicBulkSelectDropdown = <template>
+  <div class="bulk-select-topics-dropdown">
+    <span class="bulk-select-topic-dropdown__count">
+      {{i18n
+        "topics.bulk.selected_count"
+        count=@bulkSelectHelper.selected.length
+      }}
+    </span>
+    <BulkSelectTopicsDropdown @bulkSelectHelper={{@bulkSelectHelper}} />
+  </div>
+</template>;
+
+export default TopicBulkSelectDropdown;
diff --git a/app/assets/javascripts/discourse/app/components/topic-list/topic-entrance.gjs b/app/assets/javascripts/discourse/app/components/topic-list/topic-entrance.gjs
new file mode 100644
index 00000000000..19b3a052308
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/topic-list/topic-entrance.gjs
@@ -0,0 +1,101 @@
+import Component from "@glimmer/component";
+import { fn } from "@ember/helper";
+import { on } from "@ember/modifier";
+import { action } from "@ember/object";
+import { service } from "@ember/service";
+import { htmlSafe } from "@ember/template";
+import DiscourseURL from "discourse/lib/url";
+import icon from "discourse-common/helpers/d-icon";
+import I18n from "discourse-i18n";
+import DMenu from "float-kit/components/d-menu";
+
+function entranceDate(dt, showTime) {
+  const today = new Date();
+
+  if (dt.toDateString() === today.toDateString()) {
+    return moment(dt).format(I18n.t("dates.time"));
+  }
+
+  if (dt.getYear() === today.getYear()) {
+    // No year
+    return moment(dt).format(
+      showTime
+        ? I18n.t("dates.long_date_without_year_with_linebreak")
+        : I18n.t("dates.long_no_year_no_time")
+    );
+  }
+
+  return moment(dt).format(
+    showTime
+      ? I18n.t("dates.long_date_with_year_with_linebreak")
+      : I18n.t("dates.long_date_with_year_without_time")
+  );
+}
+
+export default class TopicEntrance extends Component {
+  @service historyStore;
+
+  get createdDate() {
+    return new Date(this.args.topic.created_at);
+  }
+
+  get bumpedDate() {
+    return new Date(this.args.topic.bumped_at);
+  }
+
+  get showTime() {
+    return (
+      this.bumpedDate.getTime() - this.createdDate.getTime() <
+      1000 * 60 * 60 * 24 * 2
+    );
+  }
+
+  get topDate() {
+    return entranceDate(this.createdDate, this.showTime);
+  }
+
+  get bottomDate() {
+    return entranceDate(this.bumpedDate, this.showTime);
+  }
+
+  @action
+  jumpTo(destination) {
+    this.historyStore.set("lastTopicIdViewed", this.args.topic.id);
+    DiscourseURL.routeTo(destination);
+  }
+
+  <template>
+    <DMenu
+      @ariaLabel={{@title}}
+      @placement="center"
+      @autofocus={{true}}
+      @triggerClass={{@triggerClass}}
+    >
+      <:trigger>
+        {{yield}}
+      </:trigger>
+
+      <:content>
+        <div id="topic-entrance" class="--glimmer">
+          <button
+            {{on "click" (fn this.jumpTo @topic.url)}}
+            aria-label="topic_entrance.sr_jump_top_button"
+            class="btn btn-default full jump-top"
+          >
+            {{icon "step-backward"}}
+            {{htmlSafe this.topDate}}
+          </button>
+
+          <button
+            {{on "click" (fn this.jumpTo @topic.lastPostUrl)}}
+            aria-label="topic_entrance.sr_jump_bottom_button"
+            class="btn btn-default full jump-bottom"
+          >
+            {{htmlSafe this.bottomDate}}
+            {{icon "step-forward"}}
+          </button>
+        </div>
+      </:content>
+    </DMenu>
+  </template>
+}
diff --git a/app/assets/javascripts/discourse/app/components/topic-list/topic-excerpt.gjs b/app/assets/javascripts/discourse/app/components/topic-list/topic-excerpt.gjs
new file mode 100644
index 00000000000..e293822bd91
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/topic-list/topic-excerpt.gjs
@@ -0,0 +1,16 @@
+import dirSpan from "discourse/helpers/dir-span";
+import i18n from "discourse-common/helpers/i18n";
+
+const TopicExcerpt = <template>
+  {{#if @topic.hasExcerpt}}
+    <a href={{@topic.url}} class="topic-excerpt">
+      {{dirSpan @topic.escapedExcerpt htmlSafe="true"}}
+
+      {{#if @topic.excerptTruncated}}
+        <span class="topic-excerpt-more">{{i18n "read_more"}}</span>
+      {{/if}}
+    </a>
+  {{/if}}
+</template>;
+
+export default TopicExcerpt;
diff --git a/app/assets/javascripts/discourse/app/components/topic-list/topic-link.gjs b/app/assets/javascripts/discourse/app/components/topic-list/topic-link.gjs
new file mode 100644
index 00000000000..3c8ccfe7dab
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/topic-list/topic-link.gjs
@@ -0,0 +1,21 @@
+import Component from "@glimmer/component";
+import { htmlSafe } from "@ember/template";
+
+export default class TopicLink extends Component {
+  get url() {
+    return this.args.topic.linked_post_number
+      ? this.args.topic.urlForPostNumber(this.args.topic.linked_post_number)
+      : this.args.topic.lastUnreadUrl;
+  }
+
+  <template>
+    <a
+      href={{this.url}}
+      data-topic-id={{@topic.id}}
+      role="heading"
+      aria-level="2"
+      class="title"
+      ...attributes
+    >{{htmlSafe @topic.fancyTitle}}</a>
+  </template>
+}
diff --git a/app/assets/javascripts/discourse/app/components/topic-list/topic-list-header-column.gjs b/app/assets/javascripts/discourse/app/components/topic-list/topic-list-header-column.gjs
new file mode 100644
index 00000000000..41793bdb292
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/topic-list/topic-list-header-column.gjs
@@ -0,0 +1,150 @@
+import Component from "@glimmer/component";
+import { on } from "@ember/modifier";
+import { action } from "@ember/object";
+import { service } from "@ember/service";
+import TopicBulkActions from "discourse/components/modal/topic-bulk-actions";
+import NewListHeaderControls from "discourse/components/topic-list/new-list-header-controls";
+import TopicBulkSelectDropdown from "discourse/components/topic-list/topic-bulk-select-dropdown";
+import concatClass from "discourse/helpers/concat-class";
+import icon from "discourse-common/helpers/d-icon";
+import i18n from "discourse-common/helpers/i18n";
+
+export default class TopicListHeaderColumn extends Component {
+  @service modal;
+  @service router;
+
+  get localizedName() {
+    if (this.args.forceName) {
+      return this.args.forceName;
+    }
+
+    return this.args.name ? i18n(this.args.name) : "";
+  }
+
+  get isSorting() {
+    return this.args.sortable && this.args.order === this.args.activeOrder;
+  }
+
+  get ariaSort() {
+    if (this.isSorting) {
+      return this.args.ascending ? "ascending" : "descending";
+    }
+  }
+
+  // TODO: this code probably shouldn't be in all columns
+  @action
+  bulkSelectAll() {
+    this.args.bulkSelectHelper.autoAddTopicsToBulkSelect = true;
+    document
+      .querySelectorAll("input.bulk-select:not(:checked)")
+      .forEach((el) => el.click());
+  }
+
+  @action
+  bulkClearAll() {
+    this.args.bulkSelectHelper.autoAddTopicsToBulkSelect = false;
+    document
+      .querySelectorAll("input.bulk-select:checked")
+      .forEach((el) => el.click());
+  }
+
+  @action
+  bulkSelectActions() {
+    this.modal.show(TopicBulkActions, {
+      model: {
+        topics: this.args.bulkSelectHelper.selected,
+        category: this.category,
+        refreshClosure: () => this.router.refresh(),
+      },
+    });
+  }
+
+  @action
+  onClick() {
+    this.args.changeSort(this.args.order);
+  }
+
+  @action
+  onKeyDown(event) {
+    if (event.key === "Enter" || event.key === " ") {
+      this.args.changeSort(this.args.order);
+      event.preventDefault();
+    }
+  }
+
+  <template>
+    <th
+      {{(if @sortable (modifier on "click" this.onClick))}}
+      {{(if @sortable (modifier on "keydown" this.onKeyDown))}}
+      data-sort-order={{@order}}
+      scope="col"
+      tabindex={{if @sortable "0"}}
+      role={{if @sortable "button"}}
+      aria-pressed={{this.isSorting}}
+      aria-sort={{this.ariaSort}}
+      class={{concatClass
+        "topic-list-data"
+        @order
+        (if @sortable "sortable")
+        (if @isSorting "sorting")
+        (if @number "num")
+      }}
+      ...attributes
+    >
+      {{#if @canBulkSelect}}
+        {{#if @showBulkToggle}}
+          <button
+            {{on "click" @bulkSelectHelper.toggleBulkSelect}}
+            title={{i18n "topics.bulk.toggle"}}
+            class="btn-flat bulk-select"
+          >
+            {{icon (if @experimentalTopicBulkActionsEnabled "tasks" "list")}}
+          </button>
+        {{/if}}
+
+        {{#if @bulkSelectEnabled}}
+          <span class="bulk-select-topics">
+            {{#if @canDoBulkActions}}
+              {{#if @experimentalTopicBulkActionsEnabled}}
+                <TopicBulkSelectDropdown
+                  @bulkSelectHelper={{@bulkSelectHelper}}
+                />
+              {{else}}
+                <button
+                  {{on "click" this.bulkSelectActions}}
+                  class="btn btn-icon no-text bulk-select-actions"
+                >{{icon "cog"}}&#8203;</button>
+              {{/if}}
+            {{/if}}
+
+            <button
+              {{on "click" this.bulkSelectAll}}
+              class="btn btn-default bulk-select-all"
+            >{{i18n "topics.bulk.select_all"}}</button>
+            <button
+              {{on "click" this.bulkClearAll}}
+              class="btn btn-default bulk-clear-all"
+            >{{i18n "topics.bulk.clear_all"}}</button>
+          </span>
+        {{/if}}
+      {{/if}}
+
+      {{#unless @bulkSelectEnabled}}
+        {{#if this.showTopicsAndRepliesToggle}}
+          <NewListHeaderControls
+            @current={{@newListSubset}}
+            @newRepliesCount={{@newRepliesCount}}
+            @newTopicsCount={{@newTopicsCount}}
+            @changeNewListSubset={{@changeNewListSubset}}
+          />
+        {{else}}
+          <span>{{this.localizedName}}</span>
+        {{/if}}
+      {{/unless}}
+
+      {{#if this.isSorting}}
+        {{icon (if @ascending "chevron-up" "chevron-down")}}
+      {{/if}}
+    </th>
+  </template>
+}
diff --git a/app/assets/javascripts/discourse/app/components/topic-list/topic-list-header.gjs b/app/assets/javascripts/discourse/app/components/topic-list/topic-list-header.gjs
new file mode 100644
index 00000000000..af24b4fefa0
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/topic-list/topic-list-header.gjs
@@ -0,0 +1,118 @@
+import { on } from "@ember/modifier";
+import PluginOutlet from "discourse/components/plugin-outlet";
+import TopicListHeaderColumn from "discourse/components/topic-list/topic-list-header-column";
+import icon from "discourse-common/helpers/d-icon";
+import i18n from "discourse-common/helpers/i18n";
+
+const TopicListHeader = <template>
+  <PluginOutlet @name="topic-list-header-before" />
+
+  {{#if @bulkSelectEnabled}}
+    <th class="bulk-select topic-list-data">
+      {{#if @canBulkSelect}}
+        <button
+          {{on "click" @bulkSelectHelper.toggleBulkSelect}}
+          title={{i18n "topics.bulk.toggle"}}
+          class="btn-flat bulk-select"
+        >
+          {{icon (if @experimentalTopicBulkActionsEnabled "tasks" "list")}}
+        </button>
+      {{/if}}
+    </th>
+  {{/if}}
+
+  <TopicListHeaderColumn
+    @order="default"
+    @category={{@category}}
+    @activeOrder={{@order}}
+    @changeSort={{@changeSort}}
+    @ascending={{@ascending}}
+    @name={{@listTitle}}
+    @bulkSelectEnabled={{@bulkSelectEnabled}}
+    @showBulkToggle={{@toggleInTitle}}
+    @canBulkSelect={{@canBulkSelect}}
+    @canDoBulkActions={{@canDoBulkActions}}
+    @showTopicsAndRepliesToggle={{@showTopicsAndRepliesToggle}}
+    @newListSubset={{@newListSubset}}
+    @newRepliesCount={{@newRepliesCount}}
+    @newTopicsCount={{@newTopicsCount}}
+    @experimentalTopicBulkActionsEnabled={{@experimentalTopicBulkActionsEnabled}}
+    @bulkSelectHelper={{@bulkSelectHelper}}
+    @changeNewListSubset={{@changeNewListSubset}}
+  />
+
+  <PluginOutlet @name="topic-list-header-after-main-link" />
+
+  {{#if @showPosters}}
+    <TopicListHeaderColumn
+      @order="posters"
+      @activeOrder={{@order}}
+      @changeSort={{@changeSort}}
+      @ascending={{@ascending}}
+      aria-label={{i18n "category.sort_options.posters"}}
+    />
+  {{/if}}
+
+  <TopicListHeaderColumn
+    @sortable={{@sortable}}
+    @number="true"
+    @order="posts"
+    @activeOrder={{@order}}
+    @changeSort={{@changeSort}}
+    @ascending={{@ascending}}
+    @name="replies"
+    aria-label={{i18n "sr_replies"}}
+  />
+
+  {{#if @showLikes}}
+    <TopicListHeaderColumn
+      @sortable={{@sortable}}
+      @number="true"
+      @order="likes"
+      @activeOrder={{@order}}
+      @changeSort={{@changeSort}}
+      @ascending={{@ascending}}
+      @name="likes"
+      aria-label={{i18n "sr_likes"}}
+    />
+  {{/if}}
+
+  {{#if @showOpLikes}}
+    <TopicListHeaderColumn
+      @sortable={{@sortable}}
+      @number="true"
+      @order="op_likes"
+      @activeOrder={{@order}}
+      @changeSort={{@changeSort}}
+      @ascending={{@ascending}}
+      @name="likes"
+      aria-label={{i18n "sr_op_likes"}}
+    />
+  {{/if}}
+
+  <TopicListHeaderColumn
+    @sortable={{@sortable}}
+    @number="true"
+    @order="views"
+    @activeOrder={{@order}}
+    @changeSort={{@changeSort}}
+    @ascending={{@ascending}}
+    @name="views"
+    aria-label={{i18n "sr_views"}}
+  />
+
+  <TopicListHeaderColumn
+    @sortable={{@sortable}}
+    @number="true"
+    @order="activity"
+    @activeOrder={{@order}}
+    @changeSort={{@changeSort}}
+    @ascending={{@ascending}}
+    @name="activity"
+    aria-label={{i18n "sr_activity"}}
+  />
+
+  <PluginOutlet @name="topic-list-header-after" />
+</template>;
+
+export default TopicListHeader;
diff --git a/app/assets/javascripts/discourse/app/components/topic-list/topic-list-item.gjs b/app/assets/javascripts/discourse/app/components/topic-list/topic-list-item.gjs
new file mode 100644
index 00000000000..8424ea20b7e
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/topic-list/topic-list-item.gjs
@@ -0,0 +1,503 @@
+import Component from "@glimmer/component";
+import { concat, hash } from "@ember/helper";
+import { on } from "@ember/modifier";
+import { action } from "@ember/object";
+import didInsert from "@ember/render-modifiers/modifiers/did-insert";
+import { service } from "@ember/service";
+import { eq, gt } from "truth-helpers";
+import PluginOutlet from "discourse/components/plugin-outlet";
+import ActionList from "discourse/components/topic-list/action-list";
+import ActivityColumn from "discourse/components/topic-list/activity-column";
+import ParticipantGroups from "discourse/components/topic-list/participant-groups";
+import PostCountOrBadges from "discourse/components/topic-list/post-count-or-badges";
+import PostersColumn from "discourse/components/topic-list/posters-column";
+import PostsCountColumn from "discourse/components/topic-list/posts-count-column";
+import TopicExcerpt from "discourse/components/topic-list/topic-excerpt";
+import TopicLink from "discourse/components/topic-list/topic-link";
+import UnreadIndicator from "discourse/components/topic-list/unread-indicator";
+import TopicPostBadges from "discourse/components/topic-post-badges";
+import TopicStatus from "discourse/components/topic-status";
+import { topicTitleDecorators } from "discourse/components/topic-title";
+import avatar from "discourse/helpers/avatar";
+import categoryLink from "discourse/helpers/category-link";
+import concatClass from "discourse/helpers/concat-class";
+import discourseTags from "discourse/helpers/discourse-tags";
+import formatDate from "discourse/helpers/format-date";
+import number from "discourse/helpers/number";
+import topicFeaturedLink from "discourse/helpers/topic-featured-link";
+import { wantsNewWindow } from "discourse/lib/intercept-click";
+import DiscourseURL, { groupPath } from "discourse/lib/url";
+import icon from "discourse-common/helpers/d-icon";
+import i18n from "discourse-common/helpers/i18n";
+import { bind } from "discourse-common/utils/decorators";
+import I18n from "discourse-i18n";
+
+export default class TopicListItem extends Component {
+  @service appEvents;
+  @service currentUser;
+  @service historyStore;
+  @service messageBus;
+  @service router;
+  @service site;
+  @service siteSettings;
+
+  constructor() {
+    super(...arguments);
+
+    if (this.includeUnreadIndicator) {
+      this.messageBus.subscribe(this.unreadIndicatorChannel, this.onMessage);
+    }
+  }
+
+  willDestroy() {
+    super.willDestroy(...arguments);
+
+    this.messageBus.unsubscribe(this.unreadIndicatorChannel, this.onMessage);
+  }
+
+  @bind
+  onMessage(data) {
+    const nodeClassList = document.querySelector(
+      `.indicator-topic-${data.topic_id}`
+    ).classList;
+
+    nodeClassList.toggle("read", !data.show_indicator);
+  }
+
+  get unreadIndicatorChannel() {
+    return `/private-messages/unread-indicator/${this.args.topic.id}`;
+  }
+
+  get includeUnreadIndicator() {
+    return typeof this.args.topic.unread_by_group_member !== "undefined";
+  }
+
+  get isSelected() {
+    return this.args.selected?.includes(this.args.topic);
+  }
+
+  get participantGroups() {
+    if (!this.args.topic.participant_groups) {
+      return [];
+    }
+
+    return this.args.topic.participant_groups.map((name) => ({
+      name,
+      url: groupPath(name),
+    }));
+  }
+
+  get newDotText() {
+    return this.currentUser?.trust_level > 0
+      ? ""
+      : I18n.t("filters.new.lower_title");
+  }
+
+  get tagClassNames() {
+    return this.args.topic.tags?.map((tagName) => `tag-${tagName}`);
+  }
+
+  get expandPinned() {
+    if (
+      !this.args.topic.pinned ||
+      (this.site.mobileView && !this.siteSettings.show_pinned_excerpt_mobile) ||
+      (this.site.desktopView && !this.siteSettings.show_pinned_excerpt_desktop)
+    ) {
+      return false;
+    }
+
+    return (
+      (this.args.expandGloballyPinned && this.args.topic.pinned_globally) ||
+      this.args.expandAllPinned
+    );
+  }
+
+  get shouldFocusLastVisited() {
+    return this.site.desktopView && this.args.focusLastVisitedTopic;
+  }
+
+  get unreadClass() {
+    return this.args.topic.unread_by_group_member ? "" : "read";
+  }
+
+  navigateToTopic(topic, href) {
+    this.historyStore.set("lastTopicIdViewed", topic.id);
+    DiscourseURL.routeTo(href || topic.url);
+  }
+
+  highlight(element, isLastViewedTopic) {
+    element.classList.add("highlighted");
+    element.setAttribute("data-islastviewedtopic", isLastViewedTopic);
+    element.addEventListener(
+      "animationend",
+      () => element.classList.remove("highlighted"),
+      { once: true }
+    );
+
+    if (isLastViewedTopic && this.shouldFocusLastVisited) {
+      element.querySelector(".main-link .title")?.focus();
+    }
+  }
+
+  @action
+  highlightIfNeeded(element) {
+    if (this.args.topic.id === this.historyStore.get("lastTopicIdViewed")) {
+      this.historyStore.delete("lastTopicIdViewed");
+      this.highlight(element, true);
+    } else if (this.args.topic.highlight) {
+      // highlight new topics that have been loaded from the server or the one we just created
+      this.args.topic.set("highlight", false);
+      this.highlight(element, false);
+    }
+  }
+
+  @action
+  onTitleFocus(event) {
+    event.target.classList.add("selected");
+  }
+
+  @action
+  onTitleBlur(event) {
+    event.target.classList.remove("selected");
+  }
+
+  @action
+  applyTitleDecorators(element) {
+    const rawTopicLink = element.querySelector(".raw-topic-link");
+
+    if (rawTopicLink) {
+      topicTitleDecorators?.forEach((cb) =>
+        cb(this.args.topic, rawTopicLink, "topic-list-item-title")
+      );
+    }
+  }
+
+  @action
+  onBulkSelectToggle(e) {
+    if (e.target.checked) {
+      this.args.selected.addObject(this.args.topic);
+
+      if (this.args.lastCheckedElementId && e.shiftKey) {
+        const bulkSelects = Array.from(
+          document.querySelectorAll("input.bulk-select")
+        );
+        const from = bulkSelects.indexOf(e.target);
+        const to = bulkSelects.findIndex(
+          (el) => el.id === this.args.lastCheckedElementId
+        );
+        const start = Math.min(from, to);
+        const end = Math.max(from, to);
+
+        bulkSelects
+          .slice(start, end)
+          .filter((el) => !el.checked)
+          .forEach((checkbox) => checkbox.click());
+      }
+
+      this.args.updateLastCheckedElementId(e.target.id);
+    } else {
+      this.args.selected.removeObject(this.args.topic);
+      this.args.updateLastCheckedElementId(null);
+    }
+  }
+
+  @action
+  click(e) {
+    if (
+      e.target.classList.contains("raw-topic-link") ||
+      e.target.classList.contains("post-activity")
+    ) {
+      if (wantsNewWindow(e)) {
+        return;
+      }
+
+      e.preventDefault();
+      this.navigateToTopic(this.args.topic, e.target.href);
+      return;
+    }
+
+    // make full row click target on mobile, due to size constraints
+    if (
+      this.site.mobileView &&
+      e.target.matches(
+        ".topic-list-data, .main-link, .right, .topic-item-stats, .topic-item-stats__category-tags, .discourse-tags"
+      )
+    ) {
+      if (wantsNewWindow(e)) {
+        return;
+      }
+
+      e.preventDefault();
+      this.navigateToTopic(this.args.topic, this.args.topic.lastUnreadUrl);
+      return;
+    }
+
+    if (
+      e.target.classList.contains("d-icon-thumbtack") &&
+      e.target.closest("a.topic-status")
+    ) {
+      e.preventDefault();
+      this.args.topic.togglePinnedForUser();
+      return;
+    }
+  }
+
+  @action
+  keyDown(e) {
+    if (e.key === "Enter" && e.target.classList.contains("post-activity")) {
+      e.preventDefault();
+      this.navigateToTopic(this.args.topic, e.target.href);
+    }
+  }
+
+  <template>
+    <tr
+      {{! template-lint-disable no-invalid-interactive }}
+      {{didInsert this.applyTitleDecorators}}
+      {{didInsert this.highlightIfNeeded}}
+      {{on "keydown" this.keyDown}}
+      {{on "click" this.click}}
+      data-topic-id={{@topic.id}}
+      role={{this.role}}
+      aria-level={{this.ariaLevel}}
+      class={{concatClass
+        "topic-list-item"
+        (if @topic.category (concat "category-" @topic.category.fullSlug))
+        (if (eq @topic @lastVisitedTopic) "last-visit")
+        (if @topic.visited "visited")
+        (if @topic.hasExcerpt "has-excerpt")
+        (if @topic.unseen "unseen-topic")
+        (if @topic.unread_posts "unread-posts")
+        (if @topic.liked "liked")
+        (if @topic.archived "archived")
+        (if @topic.bookmarked "bookmarked")
+        (if @topic.pinned "pinned")
+        (if @topic.closed "closed")
+        this.tagClassNames
+      }}
+    >
+      <PluginOutlet
+        @name="above-topic-list-item"
+        @outletArgs={{hash topic=@topic}}
+      />
+      {{#if this.site.desktopView}}
+        <PluginOutlet @name="topic-list-before-columns" />
+
+        {{#if @bulkSelectEnabled}}
+          <td class="bulk-select topic-list-data">
+            <label for="bulk-select-{{@topic.id}}">
+              <input
+                {{on "click" this.onBulkSelectToggle}}
+                checked={{this.isSelected}}
+                type="checkbox"
+                id="bulk-select-{{@topic.id}}"
+                class="bulk-select"
+              />
+            </label>
+          </td>
+        {{/if}}
+
+        <td class="main-link clearfix topic-list-data" colspan="1">
+          <PluginOutlet @name="topic-list-before-link" />
+
+          <span class="link-top-line">{{! no whitespace
+          }}<PluginOutlet
+              @name="topic-list-before-status"
+            />{{! no whitespace
+          }}<TopicStatus
+              @topic={{@topic}}
+            />{{! no whitespace
+          }}<TopicLink
+              {{on "focus" this.onTitleFocus}}
+              {{on "blur" this.onTitleBlur}}
+              @topic={{@topic}}
+              class="raw-link raw-topic-link"
+            />
+            {{~#if @topic.featured_link~}}
+              &nbsp;
+              {{~topicFeaturedLink @topic}}
+            {{~/if~}}
+            <PluginOutlet
+              @name="topic-list-after-title"
+            />{{! no whitespace
+          }}
+            <UnreadIndicator
+              @includeUnreadIndicator={{this.includeUnreadIndicator}}
+              @topicId={{@topic.id}}
+              class={{this.unreadClass}}
+            />
+            {{~#if @showTopicPostBadges~}}
+              <TopicPostBadges
+                @unreadPosts={{@topic.unread_posts}}
+                @unseen={{@topic.unseen}}
+                @newDotText={{this.newDotText}}
+                @url={{@topic.lastUnreadUrl}}
+              />
+            {{~/if~}}
+          </span>
+
+          <div class="link-bottom-line">
+            {{#unless @hideCategory}}
+              {{#unless @topic.isPinnedUncategorized}}
+                <PluginOutlet @name="topic-list-before-category" />
+                {{categoryLink @topic.category}}
+              {{/unless}}
+            {{/unless}}
+
+            {{discourseTags @topic mode="list" tagsForUser=@tagsForUser}}
+
+            {{#if this.participantGroups}}
+              <ParticipantGroups @groups={{this.participantGroups}} />
+            {{/if}}
+
+            <ActionList
+              @topic={{@topic}}
+              @postNumbers={{@topic.liked_post_numbers}}
+              @icon="heart"
+              class="likes"
+            />
+          </div>
+
+          {{#if this.expandPinned}}
+            <TopicExcerpt @topic={{@topic}} />
+          {{/if}}
+
+          <PluginOutlet @name="topic-list-main-link-bottom" />
+        </td>
+
+        <PluginOutlet @name="topic-list-after-main-link" />
+
+        {{#if @showPosters}}
+          <PostersColumn @posters={{@topic.featuredUsers}} />
+        {{/if}}
+
+        <PostsCountColumn @topic={{@topic}} />
+
+        {{#if @showLikes}}
+          <td class="num likes topic-list-data">
+            {{#if (gt @topic.like_count 0)}}
+              <a href={{@topic.summaryUrl}}>
+                {{number @topic.like_count}}
+                {{icon "heart"}}
+              </a>
+            {{/if}}
+          </td>
+        {{/if}}
+
+        {{#if @showOpLikes}}
+          <td class="num likes">
+            {{#if (gt @topic.op_like_count 0)}}
+              <a href={{@topic.summaryUrl}}>
+                {{number @topic.op_like_count}}
+                {{icon "heart"}}
+              </a>
+            {{/if}}
+          </td>
+        {{/if}}
+
+        <td class={{concatClass "num views topic-list-data" @topic.viewsHeat}}>
+          <PluginOutlet @name="topic-list-before-view-count" />
+          {{number @topic.views numberKey="views_long"}}
+        </td>
+
+        <ActivityColumn @topic={{@topic}} class="num topic-list-data" />
+
+        <PluginOutlet @name="topic-list-after-columns" />
+      {{else}}
+        <td class="topic-list-data">
+          <PluginOutlet @name="topic-list-before-columns" />
+
+          <div class="pull-left">
+            {{#if @bulkSelectEnabled}}
+              <label for="bulk-select-{{@topic.id}}">
+                <input
+                  type="checkbox"
+                  id="bulk-select-{{@topic.id}}"
+                  class="bulk-select"
+                />
+              </label>
+            {{else}}
+              <a
+                href={{@topic.lastPostUrl}}
+                aria-label={{i18n
+                  "latest_poster_link"
+                  username=@topic.lastPosterUser.username
+                }}
+                data-user-card={{@topic.lastPosterUser.username}}
+              >{{avatar @topic.lastPosterUser imageSize="large"}}</a>
+            {{/if}}
+          </div>
+
+          <div
+            class="topic-item-metadata right"
+          >{{! no whitespace
+            }}<PluginOutlet
+              @name="topic-list-before-link"
+            />
+
+            <div
+              class="main-link"
+            >{{! no whitespace
+              }}<PluginOutlet
+                @name="topic-list-before-status"
+              />{{! no whitespace
+              }}<TopicStatus
+                @topic={{@topic}}
+              />{{! no whitespace
+              }}<TopicLink
+                {{on "focus" this.onTitleFocus}}
+                {{on "blur" this.onTitleBlur}}
+                @topic={{@topic}}
+                class="raw-link raw-topic-link"
+              />
+              {{~#if @topic.featured_link~}}
+                {{topicFeaturedLink @topic}}
+              {{~/if~}}
+              <PluginOutlet @name="topic-list-after-title" />
+              {{~#if @topic.unseen~}}
+                <span class="topic-post-badges">&nbsp;<span
+                    class="badge-notification new-topic"
+                  ></span></span>
+              {{~/if~}}
+              {{~#if this.expandPinned~}}
+                <TopicExcerpt @topic={{@topic}} />
+              {{~/if~}}
+              <PluginOutlet @name="topic-list-main-link-bottom" />
+            </div>{{! no whitespace
+            }}<PluginOutlet
+              @name="topic-list-after-main-link"
+            />
+
+            <div class="pull-right">
+              <PostCountOrBadges
+                @topic={{@topic}}
+                @postBadgesEnabled={{@showTopicPostBadges}}
+              />
+            </div>
+
+            <div class="topic-item-stats clearfix">
+              <span class="topic-item-stats__category-tags">
+                {{#unless @hideCategory}}
+                  <PluginOutlet @name="topic-list-before-category" />
+                  {{categoryLink @topic.category}}
+                {{/unless}}
+
+                {{discourseTags @topic mode="list"}}
+              </span>
+
+              <div class="num activity last">
+                <span title={{@topic.bumpedAtTitle}} class="age activity">
+                  <a href={{@topic.lastPostUrl}}>{{formatDate
+                      @topic.bumpedAt
+                      format="tiny"
+                      noTitle="true"
+                    }}</a>
+                </span>
+              </div>
+            </div>
+          </div>
+        </td>
+      {{/if}}
+    </tr>
+  </template>
+}
diff --git a/app/assets/javascripts/discourse/app/components/topic-list/unread-indicator.gjs b/app/assets/javascripts/discourse/app/components/topic-list/unread-indicator.gjs
new file mode 100644
index 00000000000..d18b6563bf8
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/topic-list/unread-indicator.gjs
@@ -0,0 +1,21 @@
+import { concat } from "@ember/helper";
+import concatClass from "discourse/helpers/concat-class";
+import icon from "discourse-common/helpers/d-icon";
+import i18n from "discourse-common/helpers/i18n";
+
+const UnreadIndicator = <template>
+  {{#if @includeUnreadIndicator~}}
+    &nbsp;<span
+      title={{i18n "topic.unread_indicator"}}
+      class={{concatClass
+        "badge badge-notification unread-indicator"
+        (concat "indicator-topic-" @topicId)
+      }}
+      ...attributes
+    >
+      {{~icon "asterisk"~}}
+    </span>
+  {{~/if}}
+</template>;
+
+export default UnreadIndicator;
diff --git a/app/assets/stylesheets/common/topic-entrance.scss b/app/assets/stylesheets/common/topic-entrance.scss
index 21fee2bc2b7..cedd7c4a1a5 100644
--- a/app/assets/stylesheets/common/topic-entrance.scss
+++ b/app/assets/stylesheets/common/topic-entrance.scss
@@ -3,24 +3,37 @@
   padding: 5px;
   background: var(--secondary);
   box-shadow: var(--shadow-card);
-  z-index: z("dropdown");
-
-  position: absolute;
   width: 133px;
   @include unselectable;
 
+  &:not(.--glimmer) {
+    z-index: z("dropdown");
+    position: absolute;
+
+    button.full .d-icon {
+      display: block;
+      width: 100%;
+    }
+  }
+
+  &.--glimmer {
+    button.full {
+      display: flex;
+      flex-direction: column;
+    }
+  }
+
   button.full {
     width: 100%;
     margin-bottom: 5px;
     flex-wrap: wrap;
 
     .d-icon {
-      display: block;
       margin: 2px auto;
-      width: 100%;
       transform: rotate(90deg);
     }
   }
+
   button.btn.jump-bottom {
     margin: 5px 0 0 0;
   }
diff --git a/app/assets/stylesheets/desktop/category-list.scss b/app/assets/stylesheets/desktop/category-list.scss
index 1356988dbdb..b769fc21e4b 100644
--- a/app/assets/stylesheets/desktop/category-list.scss
+++ b/app/assets/stylesheets/desktop/category-list.scss
@@ -85,6 +85,12 @@
     .title {
       margin-right: 5px;
     }
+
+    &.--glimmer button.-trigger {
+      background: transparent;
+      border: none;
+      padding: 0;
+    }
   }
 
   tbody {
diff --git a/app/serializers/current_user_serializer.rb b/app/serializers/current_user_serializer.rb
index aba648259d0..d9e972ddbbf 100644
--- a/app/serializers/current_user_serializer.rb
+++ b/app/serializers/current_user_serializer.rb
@@ -74,7 +74,8 @@ class CurrentUserSerializer < BasicUserSerializer
              :new_new_view_enabled?,
              :use_experimental_topic_bulk_actions?,
              :use_admin_sidebar,
-             :can_view_raw_email
+             :can_view_raw_email,
+             :use_glimmer_topic_list?
 
   delegate :user_stat, to: :object, private: true
   delegate :any_posts, :draft_count, :pending_posts_count, :read_faq?, to: :user_stat
@@ -314,4 +315,8 @@ class CurrentUserSerializer < BasicUserSerializer
   def can_view_raw_email
     scope.user.in_any_groups?(SiteSetting.view_raw_email_allowed_groups_map)
   end
+
+  def use_glimmer_topic_list?
+    scope.user.in_any_groups?(SiteSetting.experimental_glimmer_topic_list_groups_map)
+  end
 end
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index dbbc913454f..ff9668126b1 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -2647,6 +2647,7 @@ en:
     enable_experimental_lightbox: "EXPERIMENTAL: Replace the default image lightbox with the revamped design."
     enable_experimental_bookmark_redesign_groups: "EXPERIMENTAL: Show a quick access menu for bookmarks on posts and a new redesigned modal"
     glimmer_header_mode: "Control whether the new 'glimmer' header implementation is used. Defaults to 'auto', which will enable automatically once all your themes and plugins are ready. https://meta.discourse.org/t/296544"
+    experimental_glimmer_topic_list_groups: "EXPERIMENTAL: Enable the new 'glimmer' topic list implementation. This implementation is under active development, and is not intended for production use. Do not develop themes/plugins against it until the implementation is finalized and announced."
     experimental_form_templates: "EXPERIMENTAL: Enable the form templates feature. <b>After enabled,</b> manage the templates at <a href='%{base_path}/admin/customize/form-templates'>Customize / Templates</a>."
     admin_sidebar_enabled_groups: "EXPERIMENTAL: Enable sidebar navigation for the admin UI for the specified groups, which replaces the top-level admin navigation buttons."
     lazy_load_categories_groups: "EXPERIMENTAL: Lazy load category information only for users of these groups. This improves performance on sites with many categories."
diff --git a/config/site_settings.yml b/config/site_settings.yml
index bb06873f237..01b93c1998e 100644
--- a/config/site_settings.yml
+++ b/config/site_settings.yml
@@ -2352,6 +2352,13 @@ developer:
     default: ""
     allow_any: false
     refresh: true
+  experimental_glimmer_topic_list_groups:
+    client: true
+    type: group_list
+    list_type: compact
+    default: ""
+    allow_any: false
+    refresh: true
   enable_experimental_lightbox:
     default: false
     client: true
diff --git a/plugins/styleguide/assets/javascripts/discourse/components/sections/molecules/topic-list-item.hbs b/plugins/styleguide/assets/javascripts/discourse/components/sections/molecules/topic-list-item.hbs
index ad8207fab20..3c7c9be2847 100644
--- a/plugins/styleguide/assets/javascripts/discourse/components/sections/molecules/topic-list-item.hbs
+++ b/plugins/styleguide/assets/javascripts/discourse/components/sections/molecules/topic-list-item.hbs
@@ -31,5 +31,9 @@
 </StyleguideExample>
 
 <StyleguideExample @title="<TopicListItem> - latest" class="half-size">
-  <LatestTopicListItem @topic={{@dummy.topic}} />
+  {{#if this.currentUser.use_glimmer_topic_list}}
+    <TopicList::LatestTopicListItem @topic={{@dummy.topic}} />
+  {{else}}
+    <LatestTopicListItem @topic={{@dummy.topic}} />
+  {{/if}}
 </StyleguideExample>
\ No newline at end of file
diff --git a/spec/system/page_objects/components/category_list.rb b/spec/system/page_objects/components/category_list.rb
index 49e000bf7d7..2fc5ce12375 100644
--- a/spec/system/page_objects/components/category_list.rb
+++ b/spec/system/page_objects/components/category_list.rb
@@ -3,6 +3,8 @@
 module PageObjects
   module Components
     class CategoryList < PageObjects::Components::Base
+      TOPIC_LIST_ITEM_SELECTOR = ".category-list.with-topics .featured-topic"
+
       def has_category?(category)
         page.has_css?("tr[data-category-id='#{category.id}']")
       end
@@ -30,6 +32,10 @@ module PageObjects
       def click_topic(topic)
         page.find("a", text: topic.title).click
       end
+
+      def topic_list_item_class(topic)
+        "#{TOPIC_LIST_ITEM_SELECTOR}[data-topic-id='#{topic.id}']"
+      end
     end
   end
 end
diff --git a/spec/system/page_objects/pages/topic.rb b/spec/system/page_objects/pages/topic.rb
index 1e7df6c93a4..2aa42d909b7 100644
--- a/spec/system/page_objects/pages/topic.rb
+++ b/spec/system/page_objects/pages/topic.rb
@@ -228,6 +228,10 @@ module PageObjects
         post_by_number(post).has_css?(".read-state.read", visible: :all, wait: 3)
       end
 
+      def has_suggested_topic?(topic)
+        page.has_css?("#suggested-topics .topic-list-item[data-topic-id='#{topic.id}']")
+      end
+
       def move_to_public_category(category)
         click_admin_menu_button
         find(".topic-admin-menu-content li.topic-admin-convert").click
diff --git a/spec/system/topic_list/glimmer_spec.rb b/spec/system/topic_list/glimmer_spec.rb
new file mode 100644
index 00000000000..09e056af88b
--- /dev/null
+++ b/spec/system/topic_list/glimmer_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+describe "glimmer topic list", type: :system do
+  fab!(:user)
+
+  before do
+    SiteSetting.experimental_glimmer_topic_list_groups = "1"
+    sign_in(user)
+  end
+
+  describe "/latest" do
+    let(:topic_list) { PageObjects::Components::TopicList.new }
+
+    it "shows the list" do
+      Fabricate.times(5, :topic)
+      visit("/latest")
+
+      expect(topic_list).to have_topics(count: 5)
+    end
+  end
+
+  describe "categories-with-featured-topics page" do
+    let(:category_list) { PageObjects::Components::CategoryList.new }
+
+    it "shows the list" do
+      SiteSetting.desktop_category_page_style = "categories_with_featured_topics"
+      category = Fabricate(:category)
+      topic = Fabricate(:topic, category: category)
+      topic2 = Fabricate(:topic)
+      CategoryFeaturedTopic.feature_topics
+
+      visit("/categories")
+
+      expect(category_list).to have_topic(topic)
+      expect(category_list).to have_topic(topic2)
+    end
+  end
+
+  describe "suggested topics" do
+    let(:topic_page) { PageObjects::Pages::Topic.new }
+
+    it "shows the list" do
+      topic = Fabricate(:post).topic
+      topic2 = Fabricate(:post).topic
+      visit(topic.relative_url)
+
+      expect(topic_page).to have_suggested_topic(topic2)
+    end
+  end
+end