to Improve Content Accessibility and Usability
Monica Powell • Remix Conf 2023
I’m Monica Powell
monica.dev • @indigitalcolor
monica.dev • @indigitalcolor
https://links.monica.dev
monica.dev • @indigitalcolor
I’m Monica Powell
Hint: 👩🏾💻 + MDX = 👩🏾🏫🪄📄
monica.dev • @indigitalcolor
What is MDX?
monica.dev • @indigitalcolor
What is MDX?
monica.dev • @indigitalcolor
👩🏾💻 + MDX = 👩🏾🏫🪄📄
monica.dev • @indigitalcolor
<TabbedContent tabs={[<FileWithIcon text="TypeScript" icon="code"/>,
<FileWithIcon text="Javascript" icon="code"/>]}>
<tab>
```ts
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
```
</tab>
<tab>
```js
const { PrismaClient } = require('@prisma/client')
const prisma = new PrismaClient()
```
</tab>
</TabbedContent>
monica.dev • @indigitalcolor
monica.dev • @indigitalcolor
monica.dev • @indigitalcolor
monica.dev • @indigitalcolor
unified is two things:
monica.dev • @indigitalcolor
Abstract Syntax Trees are more verbose version of human-readable code
💡 remark plugins transform markdown and rehype plugins transform HTML.
monica.dev • @indigitalcolor
monica.dev • @indigitalcolor
Built-In MDX Support | Advanced Setup |
---|---|
Add .mdx files to routes | Use a tool like mdx-bundler |
Build MDX files at build-time | Build MDX files on demand |
Quick Start ⚡️ | More Customizable 🪄 |
monica.dev • @indigitalcolor
monica.dev • @indigitalcolor
monica.dev • @indigitalcolor
monica.dev • @indigitalcolor
Write MDX
import FancyReactComponent from "~/components/FancyReactComponent"
# Hello, Remix Conf
This is just some plain ole' Markdown
<FancyReactComponent/>
monica.dev • @indigitalcolor
Write MDX
import FancyReactComponent from "~/components/FancyReactComponent"
# Hello, Remix Conf
This is just some plain ole' Markdown
<FancyReactComponent/>
📦 with
Remix without a tool like mdx-bundler
we can use MDX by:
.mdx
route module.mdx
into route modulesloader
, action
, and handle
monica.dev • @indigitalcolor
Write MDX
use Loader function fetch transformed MDX
import FancyReactComponent from "~/components/FancyReactComponent"
# Hello, Remix Conf
This is just some plain ole' Markdown
<FancyReactComponent/>
const loader = async ({params}) => {
...
return json({post})
}
app/routes/blog.$.tsx
Write MDX
use Loader function fetch transformed MDX
Render MDX Component
import FancyReactComponent from "~/components/FancyReactComponent"
# Hello, Remix Conf
This is just some plain ole' Markdown
<FancyReactComponent/>
const loader = async ({params}) => {
...
return json({post})
}
app/routes/blog.$.tsx
monica.dev • @indigitalcolor
import { bundleMDX } from "mdx-bundler";
export async function parseMdx(mdx: string) {
const { frontmatter, code } = await bundleMDX({
source: mdx,
mdxOptions(options) {
options.remarkPlugins = [...(options.remarkPlugins ?? [])];
options.rehypePlugins = [...(options.rehypePlugins ?? [])];
return options;
},
});
return {
frontmatter,
code
};
}
mdx-bundler.server.ts
monica.dev • @indigitalcolor
import { parseMdx } from "~/utils/mdx-bundler.server";
import { getContent } from "~/utils/blog.server";
export const loader = async ({params}: LoaderArgs) => {
let path = params["*"];
invariant(path, "BlogPost: path is required");
const files = await getContent(`docs/${path}`);
let post = files && (await parseMdx(files[0].content));
if (!post) {
throw json({}, {status: 404})
}
return json({post})
}
app/routes/blog.$.tsx
monica.dev • @indigitalcolor
import { useLoaderData } from '@remix-run/react'
import { getMDXComponent } from 'mdx-bundler/client';
export default function BlogPost() {
const { post } = useLoaderData<typeof loader>();
const { code } = post;
const Component = React.useMemo(() => getMDXComponent(code), [code]);
return (
<article>
<Component/>
</article>
);
}
app/routes/blog.$.tsx
monica.dev • @indigitalcolor
<Component components={{
a: CustomLink,
blockquote: CustomBlockQuote,
br: CustomBr,
code: CustomCode,
em: CustomEm,
h1: CustomHOne
h2: CustomHTwo
h3: CustomHThree,
h4: CustomHFour,
h5: CustomH5,
h6: CustomH6,
hr: CustomHr,
img: CustomImg,
li: CustomListItem,
ol: CustomOrderedList,
p: CustomParagraph,
pre: CustomPre,
strong: CustomStrong,
ul: CustomUnorderedList}} />
monica.dev • @indigitalcolor
<Component
components={{
a: CustomLink,
p: CustomParagraph
}}
/>
monica.dev • @indigitalcolor
const CustomParagraph = (props: Props): JSX.Element => {
if (typeof props.children !== 'string') {
return <>{props.children}</>
}
return <> <p {...props} /><p>✨Sparkles Powered By: MDX✨</p></>
}
monica.dev • @indigitalcolor
Wrap links in additional markup to identify external links will open in a new window and use Remix Links for internal links
monica.dev • @indigitalcolor
import { Link } from "@remix-run/react";
export const CustomLink = (props: {
href: string;
children?: React.ReactNode;
}) => {
const {href} = props;
const isInternalLink = href && (href.startsWith('/') || href.startsWith('#'))
return isInternalLink ? <Link to={href} prefetch="intent" {...props} /> : <>
<a {...props} target="_blank" rel="noopener noreferrer" />
<span className="sr-only">(opens in a new tab)</span>
</>
}
monica.dev • @indigitalcolor
[blog](/blog)
/* [blog](/blog) */
<a href="/blog">blog</a>
# on hover
<a href="/blog">blog</a><link rel="prefetch" as="fetch" href="/blog? data=routes%2Fblog">
<link rel="modulepreload" href="/build/routes/blog-QLALKNZQ_.js"><link rel="modulepreload" href=
"/build/ shared/chunk-JLXQF5V2.js">
Markdown HTML in Browser
monica.dev • @indigitalcolor
/*[Remix Conf](https://remix.run/conf)*/
<a href="https://remix.run/conf" target="_blank" rel="noopener noreferrer">Remix Conf</a>
<span class="sr-only">(opens in a new tab)</span>
[Remix Conf](https://remix.run/conf)
Markdown HTML in Browser
Use the HTML element that best reflects the markup. Be specific!
Optimize for "human-readable"
<header> and <section> to group elements conveys more meaning than <span> and <div>
Use text when possible instead of images of text
monica.dev • @indigitalcolor
monica.dev • @indigitalcolor
Unified is an ecosystem of 500+ packages that can be used to parse and manipulate ASTs.
monica.dev • @indigitalcolor
Unified is an ecosystem of 500+ packages that can be used to parse and manipulate ASTs.
Bonus:
monica.dev • @indigitalcolor
Unified is an ecosystem of 500+ packages that can be used to parse and manipulate ASTs.
How Users Read on the Web Summary:
They don't. People rarely read Web pages word by word; instead, they scan the page, picking out individual words and sentences.
- Jakob Nielsen, Nielsen Norman Group
monica.dev • @indigitalcolor
Text
export function TableOfContents({ headings }) {
if (!headings) return <></>
return (
<>
<h2> Table of Contents</h2>
<ul>{headings.map(({ id, text }) =>
id && text ?
<li key={id}>
<a href={`#${id}`}>{text}</a>
</li> : <></>
)}
</ul>
</>)
}
monica.dev • @indigitalcolor
Let's write a Rehype plugin to return all of the H2 headings in our MDX file
# Table of Contents 101
<TableOfContents/>
The above Table of Contents was generated by using MDX to parse headings using the below custom Remark plugin. A table of contents on a website is helpful for many of the same reasons that it is helpful in a document or book.
## Navigation
A table of contents can help website visitors to navigate the site quickly and easily. It provides an overview of the content and enables visitors to find the information they need without having to scroll through the entire page or site.
## Accessibility
A table of contents can improve website accessibility by providing an overview of the content that is organized in a clear and logical way. This can make the site more accessible to visitors with visual or cognitive impairments who may have difficulty navigating the site.
## User Experience
User experience: A table of contents can enhance the user experience by making it easier for visitors to find what they are looking for. This can increase visitor satisfaction and improve the chances that visitors will return to the site in the future.
monica.dev • @indigitalcolor
Text
export default function rehypeHeadings(options) {
return async function transform(tree: M.Root) {
const { visit } = await import('unist-util-visit');
visit(
tree, { type: 'element', tagName: "h2" },
function visitor(node) {
const { properties, children } = node;
const anchor = children.find((child) => child.tagName === "a")
const anchorText = anchor.children.find((child) => child.type === 'text').value
if (anchorText && properties?.id)
options.exportRef.push({
id: properties?.id,
text: anchorText
})
})
}
}
app/utils/parse-headings.ts
monica.dev • @indigitalcolor
Text
# Table of Contents 101
<TableOfContents/>
The above Table of Contents was generated by using MDX to parse headings using the below custom Remark plugin. A table of contents on a website is helpful for many of the same reasons that it is helpful in a document or book.
## Navigation
A table of contents can help website visitors to navigate the site quickly and easily. It provides an overview of the content and enables visitors to find the information they need without having to scroll through the entire page or site.
## Accessibility
A table of contents can improve website accessibility by providing an overview of the content that is organized in a clear and logical way. This can make the site more accessible to visitors with visual or cognitive impairments who may have difficulty navigating the site.
## User Experience
User experience: A table of contents can enhance the user experience by making it easier for visitors to find what they are looking for. This can increase visitor satisfaction and improve the chances that visitors will return to the site in the future.
[
{
"id": "accessibility",
"text": "Accessibility"
},
{
"id": "navigation",
"text": "Navigation"
},
{
"id": "user-experience",
"text": "User Experience"
}
]
monica.dev • @indigitalcolor
Text
import rehypeHeadings from "./parseHeadings";
export async function parseMdx(mdx: string) {
let headings = []
const { frontmatter, code } = await bundleMDX({
source: mdx,
mdxOptions(options) {
options.remarkPlugins = [ ...(options.remarkPlugins ?? [])];
options.rehypePlugins = [...(options.rehypePlugins ?? []),
rehypeSlug, [rehypeAutolinkHeadings, { behavior: "wrap"}], [rehypeHeadings, {exportRef: headings}],
];
return options;
},
});
return {
headings,
frontmatter,
code,
};
}
Text
export default function BlogPost() {
const { post } = useLoaderData<typeof loader>();
const { code, headings } = post;
const Component = React.useMemo(() => getMDXComponent(code), [code]);
return (
<article>
<Component components={{TableOfContents: () => <TableOfContents headings={headings}/>}}/>
</article>
);
}
monica.dev • @indigitalcolor
monica.dev • @indigitalcolor
monica.dev • @indigitalcolor
<p><a
id="the-world-wide-web-ref"
href="#the-world-wide-web-note"
role="doc-noteref"
aria-describedby="footnotes-label">
The World Wide Web</a>, commonly known as the web, is an integral part of modern life...
</p>
Further reading: Accessible footnotes and a bit of React by Kitty Giraudel
monica.dev • @indigitalcolor
<footer role="doc-endnotes">
<h2 id="footnotes-label">Footnotes</h2>
<ol> <li id="the-world-wide-web-note">
"World Wide Web." Wikipedia, Wikimedia Foundation, 23 Apr. 2022"<a href="#the-world-wide-web-ref" aria-label="Back to reference 1" role="doc-backlink">↩</a></li>
</ol>
</footer>
Footnote Ref
Footnote
CommonMark - version of Markdown used by MDX does not natively support Footnotes
Write HTML a11y markup for every individual footnote + ref
monica.dev • @indigitalcolor
CommonMark - version of Markdown used by MDX does not natively support Footnotes
Write HTML a11y markup for every individual footnote + ref
Create a custom <Footnotes/> component to use as needed
monica.dev • @indigitalcolor
CommonMark - version of Markdown used by MDX does not natively support Footnotes
Write HTML a11y markup for every individual footnote + ref
Create a custom <Footnotes/> component to use as needed
Extend our version of Markdown to use GitHub Flavored Markdown (b.k.a GFM)
monica.dev • @indigitalcolor
Using react-a11y-footnotes
Text
import { FootnotesProvider, FootnoteRef, Footnotes } from 'react-a11y-footnotes'
const ourComponent = () => {
return (
<FootnotesProvider>
<p> Maintaining <FootnoteRef description='additional information that is not directly related to the main text but is still relevant or useful to the reader.'>
footnotes </FootnoteRef> manually can be a pain. </p>
<Footnotes />
</FootnotesProvider>
)
}
monica.dev • @indigitalcolor
return (
<article>
<FootnotesProvider>
<Component components={{
FootnoteRef,
Footnotes }} />
</FootnotesProvider>
</article>
);
monica.dev • @indigitalcolor
<FootnoteRef description={<>"World Wide Web." Wikipedia, Wikimedia Foundation, 23 Apr. 2022, <a href="https://en.wikipedia.org/wiki/World_Wide_Web">en.wikipedia.org/wiki/World_Wide_Web</a>.'</>}>The World Wide Web</FootnoteRef>, commonly known as the web, is an integral part of modern life. It is a global network of information and resources accessible through the Internet. The web was invented in 1989 by British computer scientist <FootnoteRef description={<>'Berners-Lee, Tim. "Information Management: A Proposal." CERN, 12 Mar. 1989, <a href="https://info.cern.ch/Proposal.html">info.cern.ch/Proposal.html</a>.'</>}>Tim Berners-Lee</FootnoteRef>, who was working at the European Organization for Nuclear Research (CERN) at the time.
<Footnotes />
monica.dev • @indigitalcolor
[unified] remark plugin to support GFM (autolink literals, footnotes, strikethrough, tables, tasklists)
/*autolink literals*/
(www.x.com)
/*footnotes*/
([^1])
/*strikethrough*/
(~~stuff~~)
/*tables*/
(| cell |…)
/*tasklists*/
(* [x])
monica.dev • @indigitalcolor
options.remarkPlugins = [
...(options.remarkPlugins ?? []), gfm-remark
];
monica.dev • @indigitalcolor
MDX is a powerful tool to enhance and customize the user experience of educational content
Text
export async function parseMarkdown(markdown: string) {
const {default: remarkGfm} = await import("remark-gfm")
const { frontmatter, code } = await bundleMDX({
source: markdown,
mdxOptions(options) {
options.remarkPlugins = [...(options.remarkPlugins ?? []), remarkGFM];
options.rehypePlugins = [...(options.rehypePlugins ?? [])];
return options;
},
});
🐦 @indigitalcolor
☁️ @monica.dev
👩🏾💻 M0nica/remix-mdx-docs
monica.dev • @indigitalcolor