Understanding Buttons and Links from Zero

Thursday 26 January 2023 · 6 min read

How can we build from scratch buttons and links that are easy to use, typesafe and modular that just make sense? Welcome to a new series on basic React Components that are built with TypeScript and TailwindCSS from the ground up. All of these will be OSS and free to use.

Disclaimer: If you aren't onboard using React, TypeScript or TailwindCSS. Please note this series includes all of them.

TL;DR: at the end of the article you can find examples of how to use both of them by themselves or to create Navigation and Footer components.

Here's what we're building:


This article was originally published at Webscope's blog. For all resources/source code/examples please visit: Understanding Buttons and Links from Zero

Maybe a hot take: let's treat our buttons and links differently, no? Let's keep things simple, a button shouldn't be a link and vice-versa.

Buttons start an action. The text on the button should tell what will happen when the user clicks on it. A Link is a reference to a resource. This can either be external (e.g. a different website) or internal (e.g. a specific element or page in the current website).

If you need to execute an action: use a button. If you need to navigate to a resource: use a link. What experience you want the user to have entirely depends on you.

Here are some decisions you need to take when making either of these:

  • Typography and color: family, size, text/background color

  • Borders and spacing: radius, width, color, padding

  • Responsive design

  • Handling hover, focus, and other states

  • Dark mode styles

Here are some personal considerations when designing a button:

  • Name, role and keyboard-focusable should all be accessible (use the right HTML elements)

  • The contrast between text and background should pass accessibility checks with both light and dark modes (you can check this with the inspection tool from your Devtools)

  • Choose borders to define the look and type of your element (Tertiary buttons sometimes only have bottom borders. Inline, external, and navigation links should look different)

  • The padding ratio should be at least a 3 to 1 (horizontal to vertical) or bigger for buttons and for links it should be at least 2 to 1.

  • At smaller viewports the buttons should take full length, the links should remain at its length and they should both be stacked (very personal opinion)

  • Hover, active, focus, and disabled states are a must and should be significantly different from each other (use the Force element state pane from your DevTools to design accordingly)

  • The dark mode should alter all the color-related properties (background, color, borders, states)

Test these elements by clicking on each of them, using the keyboard's Tab key to toggle focus between them, change the viewport's size and enable dark mode all to see how these iterations work.

Here's a Button element TailwindPlayground:


Here's a Links Anchor element TailwindPlayground:


Creating a Button Component

A button component will allow you to use all of the design decisions made until now, integrate variants for different button types and also quickly consume the component around your code. Let's take a look at the decisions you need to make:

  • What props definitions do you need to create vs which ones can you reuse (more is less here, the more you can inherit from the button element, the better)

  • How will you allow your custom props (let's call these variants) to interact with each other (will you need discriminated unions or will your props overlap with each other by design)

  • What variants will you define as default (the fewer props you need to declare when using your custom component, the better)

Our goal here is to create a button component that can handle the following markup with typesafety:

import Button from "@/components/Button";

/* All buttons should be able to use the button attributes */
export default function Buttons(){
    return (
            /* When variant isn't define the button
               should default to primary */
            <Button icon >Submit</Button>
            /* Only primary buttons can have an icon */
            <Button variant="secondary" >Save</Button>
            <Button variant="tertiary" >Cancel</Button>

Here you need to decide what is it that you need. It's all up to you, but for now let's create three variants ("primary", "secondary", and "tertiary") and an icon prop (that defaults to an arrow on the right side of the button) that can only be used when the variant is primary.

/* Most component won't need to obtain a ref for its inner element  */
type ButtonProps = React.ComponentPropsWithoutRef<"button"> &
    /* 1. A discriminated union with a never type will signal
       when should the icon prop be used.
       2. Partials constructs all of its properties as optional.
        | {
              variant: "primary";
              icon: boolean;
        | {
              variant: "secondary" | "tertiary";
              icon: never;

export default function Button({
    variant = "primary",
}: ButtonProps){
    return (
                ${variant === "primary" ? "..." : ""}
                ${variant === "secondary" ? "..." : ""}
                ${variant === "tertiary" ? "..." : ""}
            {variant === "primary" && icon && (
                <svg aria-hidden>{...}</svg>

If you need to reference the full example visit: Understanding Buttons and Links from Zero

All starting considerations from the button component apply here too. Our goal here is to create a link component (will call anchor component to avoid name conflicts with framework components) that can handle the following markup with typesafety:

import Anchor from "@/components/Anchor";

/* All links should be able to use the anchor attributes */
export default function Links(){
    return (
            /* Only primary Anchors can have an icon */
            <Anchor variant="nav" href="#">Navigation</Anchor>

Here we'll create two variants ("external", "nav") and and add an icon conditionally without a prop and depending on which variant is used.

type AnchorProps = React.ComponentPropsWithoutRef<"a"> & {
    variant?: "external" | "nav";

/* No need to worry about all of the additional props */
export default function Anchor({ variant, ...props }: AnchorProps) {
    return (
                ${variant === "external" ? "..." : ""}
                ${variant === "nav" ? "..." : ""}
            {variant && (
                    className={`${variant === "external" ? "..." : "..."}`} 

If you need to reference the full example visit: Understanding Buttons and Links from Zero

Here's an example project for you to take a look on more implementation details: GitHub Repo. Visit the Components folder to see how these elements can be created using React, TypeScript and TailwindCSS.

Thanks for reading, until next time!