26 January 2023
Hector Sosa

Understanding Buttons and Links from Zero

Understanding Buttons and Links from Zero
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:

Buttons vs Links

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.

Do you need a reliable partner in tech for your next project?

What to consider - Buttons and Links

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:
1import Button from '@/components/Button';
3/* All buttons should be able to use the button attributes */
4export default function Buttons() {
5  return (
6    <>
7      /* When variant isn't define the button should default to primary */
8      <Button icon>Submit</Button>
9      /* Only primary buttons can have an icon */
10      <Button variant="secondary">Save</Button>
11      <Button variant="tertiary">Cancel</Button>
12    </>
13  );
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.
1/* Most component won't need to obtain a ref for its inner element  */
2type ButtonProps = React.ComponentPropsWithoutRef<'button'> &
3  /* 1. A discriminated union with a never type will signal
4       when should the icon prop be used.
5       2. Partials constructs all of its properties as optional.
6    */
7  Partial<
8    | {
9        variant: 'primary',
10        icon: boolean,
11      }
12    | {
13        variant: 'secondary' | 'tertiary',
14        icon: never,
15      }
16  >;
18export default function Button({
19  variant = "primary",
20  icon,
21  ...props
22}: ButtonProps){
23  return (
24    <button
25      {...props}
26      className={`
27        ...defaultStyles
28        ${variant === "primary" ? "..." : ""}
29        ${variant === "secondary" ? "..." : ""}
30        ${variant === "tertiary" ? "..." : ""}
31      `}
32    >
33      {props.children}
34      {variant === "primary" && icon && (
35        <svg aria-hidden>{...}</svg>
36      )}
37    </button>
38  )
If you need to reference the full example visit: Button Component

Creating a Link Component

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:
1import Anchor from '@/components/Anchor';
3/* All links should be able to use the anchor attributes */
4export default function Links() {
5  return (
6    <>
7      /* Only primary Anchors can have an icon */
8      <Anchor variant="external" href="#" target="_blank" rel="noreferrer">
9        External
10      </Anchor>
11      <Anchor variant="nav" href="#">
12        Navigation
13      </Anchor>
14    </>
15  );
Here we'll create two variants ("external", "nav") and and add an icon conditionally without a prop and depending on which variant is used.
1type AnchorProps = React.ComponentPropsWithoutRef<"a"> & {
2  variant?: "external" | "nav";
5/* No need to worry about all of the additional props */
6export default function Anchor({ variant, ...props }: AnchorProps) {
7  return (
8    <a
9      {...props}
10      className={`
11        ...defaultStyles
12        ${variant === "external" ? "..." : ""}
13        ${variant === "nav" ? "..." : ""}
14      `}
15    >
16      {props.children}
17      {variant && (
18        <svg 
19          aria-hidden 
20          className={`${variant === "external" ? "..." : "..."}`} 
21        >{...}
22        </svg>
23      )}
24    </a>
25  )
If you need to reference the full example visit: Anchor Component
Here's an example project for you to take a look on more implementation details: NulaCSS. Visit the Components folder to see how these elements can be created using React, TypeScript and TailwindCSS.
Thanks for reading, until next time!
Share post

Let’s stay connected

Do you want the latest and greatest from our blog straight to your inbox? Chuck us your email address and get informed.
You can unsubscribe any time. For more details, review our privacy policy