diff --git a/data/gtk/help-overlay.blp b/data/gtk/help-overlay.blp
index 4d8c3dd008b9711ce4dde6d99d08280aa2406a87..e38269136858ab36e9fa0018e12ff76d86608e8c 100644
--- a/data/gtk/help-overlay.blp
+++ b/data/gtk/help-overlay.blp
@@ -27,11 +27,6 @@ ShortcutsWindow help_overlay {
         title: "Decrement score";
         accelerator: "<Ctrl>Down <Ctrl>Left";
       }
-
-      ShortcutsShortcut {
-        title: "Show try filter";
-        accelerator: "<Ctrl>f";
-      }
     }
   }
 }
diff --git a/data/gtk/window.blp b/data/gtk/window.blp
index 4b9c9ce8cf20419d296199490cebb1a2e475fff9..01915c1b7c0cb3ef673faed8beecb2ff2aee827e 100644
--- a/data/gtk/window.blp
+++ b/data/gtk/window.blp
@@ -22,12 +22,6 @@ template $RugbyAppWindow : Adw.ApplicationWindow {
         tooltip-text: "Score";
       }
 
-      ToggleButton filter_toggle {
-        action-name: "win.show-try-filter";
-        icon-name: "funnel";
-        tooltip-text: "Show Try Filter (Ctrl+F)";
-      }
-
       [end]
       MenuButton {
         direction: none;
@@ -40,25 +34,6 @@ template $RugbyAppWindow : Adw.ApplicationWindow {
     content: Box {
       orientation: vertical;
 
-      Revealer {
-        halign: center;
-        margin-top: 6;
-        reveal-child: bind filter_toggle.active;
-        transition-type: slide_down;
-
-        SpinButton tryspin {
-          adjustment: Adjustment {
-            step-increment: 1;
-            lower: 0;
-            upper: 40;
-          };
-          focusable: true;
-          tooltip-text: "Tries";
-
-          value-changed => $try_spin_value_changed_cb();
-        }
-      }
-
       Stack stack {
 
         StackPage {
@@ -79,8 +54,6 @@ template $RugbyAppWindow : Adw.ApplicationWindow {
               tightening-threshold: 400;
 
               ListView listview {
-                styles ["rich-list", "card"]
-
                 factory: BuilderListItemFactory {
                   template ListItem {
                     child: $RugbyPossibilityWidget {
@@ -89,16 +62,23 @@ template $RugbyAppWindow : Adw.ApplicationWindow {
                     };
                   }
                 };
-                model: NoSelection {
-                  model: FilterListModel {
-                    filter: CustomFilter try_filter {};
-                    model: $RugbyListStore list_store {
-                      score: bind scorespin.value;
+                header-factory: BuilderListItemFactory {
+                  template ListHeader {
+                    child: Label {
+                      label: bind $header_label_cb(template.item) as <string>;
+                      halign: start;
                     };
+                  }
+                };
+                model: NoSelection {
+                  model: $RugbyListStore list_store {
+                    score: bind scorespin.value;
 
                     items-changed => $list_store_items_changed_cb();
                   };
                 };
+
+                styles ["rich-list", "card"]
               }
             }
           };
@@ -120,10 +100,6 @@ template $RugbyAppWindow : Adw.ApplicationWindow {
       arguments: "'down'";
       trigger: "<Ctrl>Down|<Ctrl>Left";
     }
-    Shortcut {
-      action: "action(win.show-try-filter)";
-      trigger: "<Ctrl>F";
-    }
   }
 }
 
diff --git a/meson.build b/meson.build
index c4a606e1c008067ee24852033cff25e5eb741eda..7d65473ad4a602258edb0d3c7390080757627934 100644
--- a/meson.build
+++ b/meson.build
@@ -19,7 +19,7 @@ desktopdir = datadir / 'applications'
 gnome = import('gnome')
 
 gio_dep = dependency('gio-2.0', version: '>= 2.76')
-gtk_dep = dependency('gtk4', version: '>= 4.11.3')
+gtk_dep = dependency('gtk4', version: '>= 4.12')
 libadwaita_dep = dependency('libadwaita-1', version: '>=1.5.beta')
 
 conf = configuration_data()
@@ -52,4 +52,3 @@ gnome.post_install(
   glib_compile_schemas: true,
   update_desktop_database: true,
 )
-
diff --git a/src/rugby-app-window.c b/src/rugby-app-window.c
index c3c41c44c5ddab6070d3b94c31ae7a2c4dacc422..c78d12d190f9727cd32696bb534f2c18069e99ec 100644
--- a/src/rugby-app-window.c
+++ b/src/rugby-app-window.c
@@ -1,5 +1,5 @@
 /*
- * SPDX-FileCopyrightText: 2017-2023 Bruce Cowan <bruce@bcowan.me.uk>
+ * SPDX-FileCopyrightText: 2017-2024 Bruce Cowan <bruce@bcowan.me.uk>
  *
  * SPDX-License-Identifier: GPL-3.0-or-later
  */
@@ -18,12 +18,9 @@ struct _RugbyAppWindow
     AdwApplicationWindow parent;
 
     GtkWidget *scorespin;
-    GtkWidget *tryspin;
     GtkWidget *stack;
 
     GSettings *win_settings;
-
-    GtkCustomFilter *try_filter;
 };
 
 G_DEFINE_FINAL_TYPE (RugbyAppWindow, rugby_app_window, ADW_TYPE_APPLICATION_WINDOW)
@@ -93,6 +90,22 @@ item_tooltip_cb (GtkListItem *item)
     return g_string_free (tooltip, FALSE);
 }
 
+static char *
+header_label_cb (GtkListItem *item)
+{
+    RugbyPossibility *possibility = gtk_list_header_get_item (GTK_LIST_HEADER (item));
+    if (!possibility)
+      return NULL;
+
+    int tries, utries;
+    g_object_get (possibility,
+                  "tries", &tries,
+                  "utries", &utries,
+                  NULL);
+    int total_tries = tries + utries;
+    return g_strdup_printf (ngettext ("%d try", "%d tries", total_tries), total_tries);
+}
+
 static void
 activate_score_changed (G_GNUC_UNUSED GSimpleAction *action,
                                       GVariant      *parameter,
@@ -111,14 +124,6 @@ activate_score_changed (G_GNUC_UNUSED GSimpleAction *action,
         g_assert_not_reached ();
 }
 
-static void
-try_spin_value_changed_cb (G_GNUC_UNUSED GtkSpinButton *btn,
-                                         void          *user_data)
-{
-    RugbyAppWindow *self = RUGBY_APP_WINDOW (user_data);
-    gtk_filter_changed (GTK_FILTER (self->try_filter), GTK_FILTER_CHANGE_DIFFERENT);
-}
-
 static void
 rugby_app_window_dispose (GObject *object)
 {
@@ -128,47 +133,6 @@ rugby_app_window_dispose (GObject *object)
     G_OBJECT_CLASS (rugby_app_window_parent_class)->dispose (object);
 }
 
-static gboolean
-try_filter_func (void *item,
-                 void *user_data)
-{
-    RugbyPossibility *possibility = RUGBY_POSSIBILITY (item);
-    RugbyAppWindow *self = RUGBY_APP_WINDOW (user_data);
-
-    int tries, utries;
-
-    g_object_get (possibility,
-                  "tries", &tries,
-                  "utries", &utries,
-                  NULL);
-
-    if ((tries + utries) == gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (self->tryspin)))
-        return TRUE;
-
-    return FALSE;
-}
-
-static void
-show_try_filter_change_state (GSimpleAction *action,
-                              GVariant      *value,
-                              void          *user_data)
-{
-    RugbyAppWindow *self = RUGBY_APP_WINDOW (user_data);
-
-    if (g_variant_get_boolean (value))
-    {
-        gtk_custom_filter_set_filter_func (self->try_filter, try_filter_func, self, NULL);
-        gtk_filter_changed (GTK_FILTER (self->try_filter), GTK_FILTER_CHANGE_MORE_STRICT);
-    }
-    else
-    {
-        gtk_custom_filter_set_filter_func (self->try_filter, NULL, NULL, NULL);
-        gtk_filter_changed (GTK_FILTER (self->try_filter), GTK_FILTER_CHANGE_LESS_STRICT);
-    }
-
-    g_simple_action_set_state (action, value);
-}
-
 static void
 rugby_app_window_init (RugbyAppWindow *self)
 {
@@ -176,7 +140,6 @@ rugby_app_window_init (RugbyAppWindow *self)
 
     const GActionEntry win_entries[] = {
         { .name = "score-changed", .activate = activate_score_changed, .parameter_type = "s" },
-        { .name = "show-try-filter", .state = "false", .change_state = show_try_filter_change_state },
     };
 
     g_action_map_add_action_entries (G_ACTION_MAP (self),
@@ -208,12 +171,9 @@ rugby_app_window_class_init (RugbyAppWindowClass *klass)
                                                  "/uk/me/bcowan/Rugby/gtk/window.ui");
 
     gtk_widget_class_bind_template_child (widget_class, RugbyAppWindow, scorespin);
-    gtk_widget_class_bind_template_child (widget_class, RugbyAppWindow, tryspin);
     gtk_widget_class_bind_template_child (widget_class, RugbyAppWindow, stack);
 
-    gtk_widget_class_bind_template_child (widget_class, RugbyAppWindow, try_filter);
-
-    gtk_widget_class_bind_template_callback (widget_class, try_spin_value_changed_cb);
+    gtk_widget_class_bind_template_callback (widget_class, header_label_cb);
     gtk_widget_class_bind_template_callback (widget_class, item_tooltip_cb);
     gtk_widget_class_bind_template_callback (widget_class, list_store_items_changed_cb);
 }
diff --git a/src/rugby-list-store.c b/src/rugby-list-store.c
index 5e518929a002efdb85a32261680c4dfcef734ed5..57ccda29ad43f35bc07477ed04683e6bfddc6c34 100644
--- a/src/rugby-list-store.c
+++ b/src/rugby-list-store.c
@@ -9,6 +9,7 @@
 #include "rugby-possibility.h"
 
 #include <gio/gio.h>
+#include <gtk/gtk.h>
 
 struct _RugbyListStore
 {
@@ -21,10 +22,14 @@ struct _RugbyListStore
 };
 
 static void rugby_list_store_list_model_iface_init (GListModelInterface *iface);
+static void rugby_list_store_section_model_iface_init (GtkSectionModelInterface *iface);
 
 G_DEFINE_FINAL_TYPE_WITH_CODE (RugbyListStore, rugby_list_store, G_TYPE_OBJECT,
                                G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL,
-                                                      rugby_list_store_list_model_iface_init))
+                                                      rugby_list_store_list_model_iface_init)
+                               G_IMPLEMENT_INTERFACE (GTK_TYPE_SECTION_MODEL,
+                                                      rugby_list_store_section_model_iface_init))
+
 
 enum
 {
@@ -108,6 +113,18 @@ process_data (RugbyListStore *self)
                                 g_list_model_get_n_items (G_LIST_MODEL (self->items)));
 }
 
+static int
+get_total_tries (RugbyPossibility *possibility)
+{
+    int tries, utries;
+
+    g_object_get (possibility,
+                  "tries", &tries,
+                  "utries", &utries,
+                  NULL);
+    return tries + utries;
+}
+
 // GListModel implementation
 
 static GType
@@ -141,6 +158,63 @@ rugby_list_store_list_model_iface_init (GListModelInterface *iface)
     iface->get_item = rugby_list_store_get_item;
 }
 
+// SectionModel implementation
+
+static void
+rugby_list_store_get_section (GtkSectionModel *model,
+                              unsigned         position,
+                              unsigned        *out_start,
+                              unsigned        *out_end)
+{
+    RugbyListStore *self = RUGBY_LIST_STORE (model);
+
+    unsigned n_items = g_list_model_get_n_items (G_LIST_MODEL (self->items));
+    RugbyPossibility *possibility = g_list_model_get_item (G_LIST_MODEL (self->items),
+                                                           position);
+    if (!possibility)
+    {
+        *out_start = n_items;
+        *out_end = G_MAXUINT;
+        return;
+    }
+
+    int target_tries = get_total_tries (possibility);
+
+    // Find start
+    for (unsigned i = 0; i < n_items; i++)
+    {
+        possibility = g_list_model_get_item (G_LIST_MODEL (self->items), i);
+        int total = get_total_tries (possibility);
+
+        if (total == target_tries)
+        {
+            *out_start = i;
+            break;
+        }
+    }
+
+    // Find end
+    for (unsigned i = *out_start + 1; i < n_items; i++)
+    {
+        possibility = g_list_model_get_item (G_LIST_MODEL (self->items), i);
+        int total = get_total_tries (possibility);
+
+        if (total != target_tries)
+        {
+            *out_end = i;
+            return;
+        }
+    }
+
+    *out_end = n_items;
+}
+
+static void
+rugby_list_store_section_model_iface_init (GtkSectionModelInterface *iface)
+{
+    iface->get_section = rugby_list_store_get_section;
+}
+
 // Class functions
 
 static void