diff --git a/packages/site_shared/lib/components/blog/blog_toc.dart b/packages/site_shared/lib/components/blog/blog_toc.dart
new file mode 100644
index 0000000000..5d4635f36c
--- /dev/null
+++ b/packages/site_shared/lib/components/blog/blog_toc.dart
@@ -0,0 +1,56 @@
+// Copyright (c) 2026, the Dart project authors. All rights reserved.
+// Copyright 2026, the Flutter authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that
+// can be found in the LICENSE file.
+
+import 'package:jaspr/dom.dart';
+import 'package:jaspr/jaspr.dart';
+import 'package:jaspr_content/jaspr_content.dart';
+
+import '../common/material_icon.dart';
+
+/// The minimum number of top-level entries an article needs to have
+/// before a table of contents is worth showing.
+const _minEntriesForToc = 2;
+
+/// Displays an automatically generated table of contents for the current
+/// page, derived from the `h2` and `h3` headings found in its rendered
+/// content.
+///
+/// Requires [TableOfContentsExtension] to be applied to the page so that
+/// its headings are available as a [TableOfContents] under the page's `toc`
+/// data.
+///
+/// Renders nothing if the page doesn't contain enough headings to warrant
+/// a table of contents.
+final class BlogTableOfContents extends StatelessComponent {
+ const BlogTableOfContents({super.key});
+
+ @override
+ Component build(BuildContext context) {
+ final toc = context.page.data['toc'] as TableOfContents?;
+ if (toc == null || toc.entries.length < _minEntriesForToc) {
+ return const Component.empty();
+ }
+
+ return nav(
+ classes: 'toc',
+ attributes: {'aria-label': 'Table of contents'},
+ [
+ Component.element(
+ tag: 'details',
+ children: [
+ const Component.element(
+ tag: 'summary',
+ children: [
+ MaterialIcon('chevron_right'),
+ Component.text('On this page'),
+ ],
+ ),
+ div(classes: 'toc-list', [toc.build()]),
+ ],
+ ),
+ ],
+ );
+ }
+}
diff --git a/packages/site_shared/lib/components/common/client/back_to_top_button.dart b/packages/site_shared/lib/components/common/client/back_to_top_button.dart
new file mode 100644
index 0000000000..90ba021a2a
--- /dev/null
+++ b/packages/site_shared/lib/components/common/client/back_to_top_button.dart
@@ -0,0 +1,31 @@
+// Copyright 2026 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:jaspr/dom.dart';
+import 'package:jaspr/jaspr.dart';
+import 'package:universal_web/web.dart' as web;
+
+import '../material_icon.dart';
+
+/// A floating button that scrolls the page back to the top when clicked.
+///
+/// Stays hidden until the page has been scrolled, via CSS rules that key
+/// off of the `in-content` class already toggled on `