Index: src/wp-admin/includes/privacy-tools.php
===================================================================
--- src/wp-admin/includes/privacy-tools.php	(revision 47110)
+++ src/wp-admin/includes/privacy-tools.php	(working copy)
@@ -234,7 +234,7 @@
  * @return string The HTML for this group and its items.
  */
 function wp_privacy_generate_personal_data_export_group_html( $group_data ) {
-	$group_html  = '<h2>';
+	$group_html  = '<h2 id="' . esc_attr( sanitize_title_with_dashes( $group_data['group_label'] ) ) . '">';
 	$group_html .= esc_html( $group_data['group_label'] );
 
 	$items_count = count( (array) $group_data['items'] );
@@ -271,8 +271,12 @@
 		$group_html .= '</table>';
 	}
 
+	$group_html .= '<div class="return_to_top">';
+	$group_html .= '<a href="#top">' . esc_html__( '&uarr; Return to top' ) . '</a>';
 	$group_html .= '</div>';
 
+	$group_html .= '</div>';
+
 	return $group_html;
 }
 
@@ -350,6 +354,7 @@
 	fwrite( $file, 'th { padding: 5px; text-align: left; width: 20%; }' );
 	fwrite( $file, 'td { padding: 5px; }' );
 	fwrite( $file, 'tr:nth-child(odd) { background-color: #fafafa; }' );
+	fwrite( $file, '.return_to_top { text-align:right; }' );
 	fwrite( $file, '</style>' );
 	fwrite( $file, '<title>' );
 	fwrite( $file, esc_html( $title ) );
@@ -360,7 +365,7 @@
 	fwrite( $file, "<body>\n" );
 
 	// Heading.
-	fwrite( $file, '<h1>' . esc_html__( 'Personal Data Export' ) . '</h1>' );
+	fwrite( $file, '<h1 id="top">' . esc_html__( 'Personal Data Export' ) . '</h1>' );
 
 	// And now, all the Groups.
 	$groups = get_post_meta( $request_id, '_export_data_grouped', true );
@@ -396,6 +401,22 @@
 	// Merge in the special about group.
 	$groups = array_merge( array( 'about' => $about_group ), $groups );
 
+	// Create TOC.
+	if ( count( $groups ) > 1 ) {
+		fwrite( $file, '<div id="table_of_contents">' );
+		fwrite( $file, '<h2>' . esc_html__( 'Table of Contents' ) . '</h2>' );
+		fwrite( $file, '<ul>' );
+
+		foreach ( (array) $groups as $group_id => $group_data ) {
+			fwrite( $file, '<li>' );
+			fwrite( $file, '<a href="#' . esc_attr( sanitize_title_with_dashes( $group_data['group_label'] ) ) . '">' . esc_html( $group_data['group_label'] ) . '</a>' );
+			fwrite( $file, '</li>' );
+		}
+
+		fwrite( $file, '</ul>' );
+		fwrite( $file, '</div>' );
+	}
+
 	// Now, iterate over every group in $groups and have the formatter render it in HTML.
 	foreach ( (array) $groups as $group_id => $group_data ) {
 		fwrite( $file, wp_privacy_generate_personal_data_export_group_html( $group_data ) );
Index: tests/phpunit/tests/privacy/wpPrivacyGeneratePersonalDataExportFile.php
===================================================================
--- tests/phpunit/tests/privacy/wpPrivacyGeneratePersonalDataExportFile.php	(revision 47110)
+++ tests/phpunit/tests/privacy/wpPrivacyGeneratePersonalDataExportFile.php	(working copy)
@@ -260,8 +260,8 @@
 		$report_contents = file_get_contents( $report_dir . 'index.html' );
 		$request         = wp_get_user_request_data( self::$export_request_id );
 
-		$this->assertContains( '<h1>Personal Data Export</h1>', $report_contents );
-		$this->assertContains( '<h2>About</h2>', $report_contents );
+		$this->assertContains( '<h1 id="top">Personal Data Export</h1>', $report_contents );
+		$this->assertContains( '<h2 id="about">About</h2>', $report_contents );
 		$this->assertContains( $request->email, $report_contents );
 	}
 }
Index: tests/phpunit/tests/privacy/wpPrivacyGeneratePersonalDataExportGroupHtml.php
===================================================================
--- tests/phpunit/tests/privacy/wpPrivacyGeneratePersonalDataExportGroupHtml.php	(revision 47110)
+++ tests/phpunit/tests/privacy/wpPrivacyGeneratePersonalDataExportGroupHtml.php	(working copy)
@@ -42,7 +42,7 @@
 		$actual                = wp_privacy_generate_personal_data_export_group_html( $data );
 		$expected_table_markup = '<table><tbody><tr><th>Field 1 Name</th><td>Field 1 Value</td></tr><tr><th>Field 2 Name</th><td>Field 2 Value</td></tr></tbody></table>';
 
-		$this->assertContains( '<h2>Test Data Group</h2>', $actual );
+		$this->assertContains( '<h2 id="test-data-group">Test Data Group</h2>', $actual );
 		$this->assertContains( $expected_table_markup, $actual );
 	}
 
@@ -81,7 +81,7 @@
 
 		$actual = wp_privacy_generate_personal_data_export_group_html( $data );
 
-		$this->assertContains( '<h2>Test Data Group', $actual );
+		$this->assertContains( '<h2 id="test-data-group">Test Data Group', $actual );
 		$this->assertContains( '<td>Field 1 Value', $actual );
 		$this->assertContains( '<td>Another Field 1 Value', $actual );
 		$this->assertContains( '<td>Field 2 Value', $actual );
@@ -131,13 +131,13 @@
 	 */
 	public function test_group_labels_escaped() {
 		$data = array(
-			'group_label' => '<div>Escape HTML in group lavels</div>',
+			'group_label' => '<div>Escape HTML in group labels</div>',
 			'items'       => array(),
 		);
 
 		$actual = wp_privacy_generate_personal_data_export_group_html( $data );
 
-		$this->assertContains( '<h2>&lt;div&gt;Escape HTML in group lavels&lt;/div&gt;</h2>', $actual );
+		$this->assertContains( '<h2 id="escape-html-in-group-labels">&lt;div&gt;Escape HTML in group labels&lt;/div&gt;</h2>', $actual );
 	}
 
 	/**
@@ -225,7 +225,7 @@
 
 		$actual = wp_privacy_generate_personal_data_export_group_html( $data );
 
-		$this->assertContains( '<h2>Test Data Group', $actual );
+		$this->assertContains( '<h2 id="test-data-group">Test Data Group', $actual );
 		$this->assertContains( '<span class="count">(2)</span></h2>', $actual );
 		$this->assertSame( 2, substr_count( $actual, '<table>' ) );
 	}
@@ -250,7 +250,7 @@
 
 		$actual = wp_privacy_generate_personal_data_export_group_html( $data );
 
-		$this->assertContains( '<h2>Test Data Group</h2>', $actual );
+		$this->assertContains( '<h2 id="test-data-group">Test Data Group</h2>', $actual );
 		$this->assertNotContains( '<span class="count">', $actual );
 		$this->assertSame( 1, substr_count( $actual, '<table>' ) );
 	}
