Sitemap

What I learned while creating a Typography component

4 min readMay 28, 2025

Typography is the unsung hero of web development. It’s everywhere, from headings to body text, yet it’s often overlooked. Ensuring that our text components are both accessible and semantically correct can significantly enhance the user experience. But how do we balance design freedom with technical constraints?

The Problem with Typography Components

Many typography components struggle with maintaining accessibility and consistency. A basic implementation like this can quickly become messy and complicated to maintain:

<Container>
<Typography variant="headline" as="h1" size={2}>
Hello world
</Typography>
<Typography variant="paragraph">
Lorem ipsum <Typography variant="bold">esse</Typography> a cumque similique
</Typography>
</Container>

Addressing Accessibility and Semantics

As a product designer, you need flexibility in font sizes and styles. As an Engineer, you need to ensure semantic HTML and accessibility. To bridge this gap, I developed a single Typography component encompassing all necessary typography elements, including a screen-reader-only text variant.

Here’s the interface for common typography props:

interface TypographyProps {
srOnly?: boolean;
size?: FontSizes;
weight?: FontWeights;
color?: Colors;
lineHeight?: CSSProperties['lineHeight'];
fontFamily?: FontFamily;
align?: CSSProperties['textAlign'];
whiteSpace?: CSSProperties['whiteSpace'];
highlight?: Colors;
}

type CommonTypographyType = HTMLAttributes<HTMLHtmlElement>
& TypographyProps
& { children?: ReactNode };

Defining Variants and Semantic Tags

The main differences in typography components are the variant presets and the semantic tags. Define types for each variant to enforce correct usage:

type BodyTextType = CommonTypographyType & {
variant?: 'body' | 'subheading';
as?: 'p' | 'span' | 'strong' | 'em';
};

type HeadlineType = CommonTypographyType & {
variant?: 'headline';
as: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
};

...

We can then unionize them using discriminated unions so that the props will change depending on the variant:

type TypographyType =
| BodyTextType
| HeadlineType
| ParagraphType
| SpanType
| CodeType;

Developers can choose a variant. They’ll get an error if they don’t use the correct props for that variant.

Press enter or click to view image in full size
Lint error with missing as for headline variant
Error — Property ‘as’ is missing in type

Separating Sizes from Semantic HTML

Separating the control of visual styles from the semantic HTML elements is crucial. This approach allows developers to define the visual presentation of text independently of its semantic meaning. For example, you can style a headline with a large font size while using a <p> tag if it fits the context better. This decoupling ensures the text remains accessible to assistive technologies, allowing designers the freedom to achieve the desired aesthetic.

Creating Shorthand Components

By splitting the six-in-one component into shorthand components, we provide presets and accessibility out of the box:

export const Headline: FC<HeadlineType> = (props) => (
<Typography variant="headline" {...props} />
);

export const Paragraph: FC<ParagraphType> = (props) => (
<Typography variant="paragraph" {...props} />
);

export const Code: FC<CodeType> = (props) => (
<Typography variant="code" {...props} />
);

...

Now, shorthands are standalone and can autocomplete missing attributes to correct errors. This saves time by eliminating the need to input the same props repeatedly.

Press enter or click to view image in full size
Drop down menu in VS Code with ‘add missing attributes’ selected
Add quickly add missing attributes in the headline shorthand

Example Usage:

Using these shorthand components, we can maintain a clean and semantic structure:

<Headline size={4} as="h1">
Hey 👋, I'm a Typography component
</Headline>
<Paragraph>
I can be a paragraph too, and you can make parts of me <Bold>bold.</Bold>
</Paragraph>
<Paragraph>
You can also write <InlineCode>inline code</InlineCode> like this.
</Paragraph>
<Paragraph>
<Code color="deep">// a code block using the <code/> shorthand</Code>
<Code>const foo = (bar) => bar >= 2;</Code>
<Headline size={3} as="h2">
I can be a headline too
</Headline>
</Paragraph>
<Paragraph>
I have preset sizes, and I am able to use semantic tags.
</Paragraph>

What’s Next? Refining the Design

Once the team agrees on presets, sizes, and guidelines, we can implement these in a markdown library, such as React Markdown. This allows for even cleaner code:

<Markdown>
{`
# Hey 👋, I'm a Typography component

I can be a paragraph too, and you can make parts of me **bold.**

You can also write \`inline code\` like this.

\`\`\`
// a code block using the <code/> shorthand
const foo = (bar) => bar >= 2;

## I can be a headline too

I have preset sizes, and I am able to use semantic tags.
`}
</Markdown>

Conclusion

Separating the control of visual styles from semantic HTML elements enables a robust design system that caters to diverse needs without compromising accessibility or aesthetics. This approach demonstrates the power of discriminated unions in TypeScript, allowing us to enforce best practices and maintain consistency across our applications. By breaking down complex components into more straightforward, manageable pieces, we can ensure that our design systems are robust, maintainable, and accessible to everyone.

As you implement these techniques in your projects, remember that accessibility and semantics are not just technical requirements — they are essential for creating inclusive and compelling user experiences. Let’s continue to push the boundaries of what’s possible with thoughtful, well-designed components that serve all users.

--

--

Elad Mizrahi
Elad Mizrahi

Written by Elad Mizrahi

I combine my knowledge in product design and front-end development to design clever components that make designers and developers work better together

No responses yet