Consistent and accessible page titles in Remix

Titles, headings, and accessibility

Success criterion 2.4.2 "Page Titled" from the WCAG 2.1 standards states

Web pages have titles that describe topic or purpose.

This title is necessary for multiple reasons, the most important being that it allows web users to know where they are without having to read the actual content of the page. The title also helps search engines know what a site is about.

Success criterion 2.4.6 "Headings and Labels" states

Headings and labels describe topic or purpose.

These headings should be hierarchical, like in an outline, so as not to confuse users. The main content section of the page should have a single H1 heading that describes its content . Typically we want this to match what the title attribute of the page states for consistency.

Keeping things consistent in Remix

In Remix, we can structure our code in a way that specifies a single title string that can be reused in both the title attribute of the page and the H1 heading, to ensure that they match.

import type { MetaFunction } from "@remix-run/node";

const title = "About";

export let meta: MetaFunction = () => {
  return {
    title,
  };
};

export default function AboutPageRoute() {
  return (
    <>
      <h1>{title}</h1>
      <p>Some content here...</p>
    </>
  );
}

Sometimes the site title is included in the title attribute for the page. In this case we could use a function to create the title for us and then reuse that function.

// utils/title.ts
const siteTitle = "Nik's Site";

export function createPageTitle(pageTitle: string) {
  return `${siteTitle} - ${pageTitle}`;
}
import type { MetaFunction } from "@remix-run/node";
import { createPageTitle } from "~/utils/title";

const title = "About";

export let meta: MetaFunction = () => {
  return {
    title: createPageTitle(title)
  };
};

...

We may want to use the same title string in a breadcrumb as well. If we use the same value from out title then if we change the title later, it will show up correctly everywhere.

Using server data in the title

Sometimes the title of the page will depend on data we are fetching from the server.

Here is one example:

The path in the URL is /students/1439 where 1439 is a student ID but we want to show the student's name in the title. Assuming the student's data is being fetched in a loader, we can get that data using the arguments to the meta function.

export let meta: MetaFunction = ({ data }) => {
  // data looks like this
  // { student: { id: "1439", name: "Jackie Green", ... }}

  // Let's use the student's name
  return {
    title: createPageTitle(data.student.name),
  };
}

We will be able to use the same loader data in the route component:

export default function StudentRoute() {
  const {
    student
  } = useLoaderData<LoaderData>();

  return (
    <>
      <h1>{student.name}</h1>
      <p>Some content here...</p>
    </>
  );
}

But how can we ensure that these are consistent?

Let's reuse a function! Here is everything put together:

// We can reuse this function in both the meta function
// and the route component because they both have 
// access to the same loader data.
function getTitle(data: LoaderData) {
  return data.student.name;
}

type LoaderData = { student: Student };

export const loader: LoaderFunction = async ({ params }) => {
  const data: LoaderData = {
    student: await getStudent(params.studentId),
  };
  return json(data);
};

export let meta: MetaFunction = ({ data }) => {
  return {
    title: createPageTitle(getTitle(data)),
  };
}

export default function StudentRoute() {
  const data = useLoaderData<LoaderData>();

  return (
    <>
      <h1>{getTitle(data)}</h1>
      <p>More content here...</p>
    </>
  );
}

If we need data from a parent component's loader, we can use that as well. In the meta function arguments, we would use parentsData and in the route component we can use the useMatches hook.

For more information check out these links: