diff --git a/frontend/src/components/BackLink.css b/frontend/src/components/BackLink.css
new file mode 100644
index 0000000..dbac36b
--- /dev/null
+++ b/frontend/src/components/BackLink.css
@@ -0,0 +1,33 @@
+/* A link styled like the other secondary buttons (Load more, Clear
+ filters). Inline-block so the padding keeps its space outside a grid
+ too, as on the submit page. */
+.back-link {
+ display: inline-block;
+ justify-self: start;
+ padding: 7px 14px;
+ border-radius: 12px;
+ color: var(--accent-strong);
+ background: var(--accent-bg);
+ font-size: 15px;
+ font-weight: 600;
+ text-decoration: none;
+ transition: background-color 0.15s;
+
+ &:hover {
+ background: var(--accent-bg-strong);
+ }
+
+ &:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+ }
+
+ /* High-contrast modes drop the tint, so a border keeps the shape. */
+ @media (forced-colors: active) {
+ border: 1px solid LinkText;
+ }
+
+ @media (prefers-reduced-motion: reduce) {
+ transition: none;
+ }
+}
diff --git a/frontend/src/components/BackLink.tsx b/frontend/src/components/BackLink.tsx
new file mode 100644
index 0000000..d1cc690
--- /dev/null
+++ b/frontend/src/components/BackLink.tsx
@@ -0,0 +1,24 @@
+import type { ReactNode } from 'react'
+import { Link, type To } from 'react-router'
+import './BackLink.css'
+
+interface BackLinkProps {
+ to: To
+ children: ReactNode
+}
+
+/**
+ * The link back to the list at the top of the detail and submit pages, so
+ * it has the same shape, spacing and arrow on both. The arrow is hidden from
+ * screen readers, which read just the link text.
+ */
+function BackLink({ to, children }: BackLinkProps) {
+ return (
+
+ ←
+ {children}
+
+ )
+}
+
+export default BackLink
diff --git a/frontend/src/pages/SightingDetailPage.css b/frontend/src/pages/SightingDetailPage.css
index 99f5692..bfe9f1d 100644
--- a/frontend/src/pages/SightingDetailPage.css
+++ b/frontend/src/pages/SightingDetailPage.css
@@ -15,38 +15,6 @@
border: 0;
}
-/* A link styled like the other secondary buttons (Load more, Clear
- filters). */
-.sighting-detail__back {
- justify-self: start;
- padding: 7px 14px;
- border-radius: 12px;
- color: var(--accent-strong);
- background: var(--accent-bg);
- font-size: 15px;
- font-weight: 600;
- text-decoration: none;
- transition: background-color 0.15s;
-
- &:hover {
- background: var(--accent-bg-strong);
- }
-
- &:focus-visible {
- outline: 2px solid var(--accent);
- outline-offset: 2px;
- }
-
- /* High-contrast modes drop the tint, so a border keeps the shape. */
- @media (forced-colors: active) {
- border: 1px solid LinkText;
- }
-
- @media (prefers-reduced-motion: reduce) {
- transition: none;
- }
-}
-
.sighting-detail h1 {
font-size: 40px;
margin: 0 0 4px;
diff --git a/frontend/src/pages/SightingDetailPage.tsx b/frontend/src/pages/SightingDetailPage.tsx
index bc1b7f8..a572bab 100644
--- a/frontend/src/pages/SightingDetailPage.tsx
+++ b/frontend/src/pages/SightingDetailPage.tsx
@@ -1,6 +1,7 @@
import { CombinedGraphQLErrors } from '@apollo/client'
import { useEffect } from 'react'
-import { Link, useLocation, useParams } from 'react-router'
+import { useLocation, useParams } from 'react-router'
+import BackLink from '../components/BackLink'
import SightingMap from '../components/SightingMap'
import UserSubmittedBadge from '../components/UserSubmittedBadge'
import { useDocumentTitle } from '../hooks/use-document-title'
@@ -51,11 +52,7 @@ function SightingDetailPage() {
useDocumentTitle(title)
- const backLink = (
-
- ← Back to results
-
- )
+ const backLink = Back to results
return (
<>
diff --git a/frontend/src/pages/SubmitSightingPage.css b/frontend/src/pages/SubmitSightingPage.css
index 5b65773..e82ebe7 100644
--- a/frontend/src/pages/SubmitSightingPage.css
+++ b/frontend/src/pages/SubmitSightingPage.css
@@ -3,13 +3,9 @@
margin: 0 auto;
}
-.submit-sighting__back {
- color: var(--accent);
- text-underline-offset: 4px;
-}
-
+/* The same 24px below the back link as on the detail page. */
.submit-sighting h1 {
- margin-bottom: 16px;
+ margin: 24px 0 16px;
}
.submit-sighting__intro {
@@ -86,8 +82,7 @@
.submit-sighting__field select:focus-visible,
.submit-sighting__field textarea:focus-visible,
.submit-sighting button:focus-visible,
-.submit-sighting__button:focus-visible,
-.submit-sighting__back:focus-visible {
+.submit-sighting__button:focus-visible {
outline: 3px solid var(--accent);
outline-offset: 2px;
}
diff --git a/frontend/src/pages/SubmitSightingPage.tsx b/frontend/src/pages/SubmitSightingPage.tsx
index 483d020..08bd82e 100644
--- a/frontend/src/pages/SubmitSightingPage.tsx
+++ b/frontend/src/pages/SubmitSightingPage.tsx
@@ -6,6 +6,7 @@ import {
CREATE_SIGHTING_MUTATION,
type SightingFieldError,
} from '../graphql/mutations'
+import BackLink from '../components/BackLink'
import LocationPicker from '../components/LocationPicker'
import { SHAPES_QUERY } from '../graphql/queries'
@@ -254,9 +255,7 @@ function SubmitSightingPage() {
if (submittedId) {
return (
-
- Back to sightings
-
+ Back to sightings
Sighting submitted
@@ -286,9 +285,7 @@ function SubmitSightingPage() {
return (
-
- Back to sightings
-
+ Back to sightings
Report a sighting
diff --git a/frontend/src/tests/BackLink.test.tsx b/frontend/src/tests/BackLink.test.tsx
new file mode 100644
index 0000000..4697ea9
--- /dev/null
+++ b/frontend/src/tests/BackLink.test.tsx
@@ -0,0 +1,20 @@
+import { render, screen } from '@testing-library/react'
+import { MemoryRouter } from 'react-router'
+import { describe, expect, it } from 'vitest'
+import BackLink from '../components/BackLink'
+
+describe('BackLink', () => {
+ it('links to its target and is named by its text, not the arrow', () => {
+ render(
+
+
+ Back to results
+
+ ,
+ )
+
+ const link = screen.getByRole('link', { name: 'Back to results' })
+ expect(link).toHaveAttribute('href', '/?q=disk')
+ expect(link).toHaveTextContent('← Back to results')
+ })
+})