Will King
Remix

Pattern: One Button to rule them all

Most apps have a set of Button styles that are universal to your app, but in a framework like Remix the underlying element could be one of 3 different options:

  • <button/>: For an on page action

  • <Link/>: For navigation within your app

  • <a/>: For navigation outside of your app

Styling is not the only place where it gets clunky trying to manage which action is being rendered. Ever written anything like this in your app?

const links = [
  {
    label: "Menu Item 1",
    link: "https://some.extrernal.site"
  },
  {
    label: "Menu Item 1",
    link: "/path/inside/app"
  },
];

function Menu() {
  return links.map(({ label, link }) => link.includes() ? (
    <a href={link}>{label}</a>
  ) : (
    <Link href={link}>{label}</Link>
  );
}

Instead of sprinkling little conditionals all over my apps I decided to come up with a pattern where I would not need to do this ever again.

The Implementation

The root of the pattern is just a raw implementation. I like to isolate implementations from styles when it makes sense for more flexibility so I put this in my ~/components/impl folder.

// ~/components/impl/Action

import type { LinkProps } from "@remix-run/react";
import type { ComponentPropsWithRef } from "react";

import { Link } from "@remix-run/react";
import { forwardRef } from "react";

type AnchorProps = React.ComponentPropsWithRef<"a">;
type ButtonProps = React.ComponentPropsWithRef<"button">;

const AAnchor = forwardRef<HTMLAnchorElement, AnchorProps>(
  ({ children, ...props }, ref) => (
    <a {...props} ref={ref}>
      {children}
    </a>
  )
);

AAnchor.displayName = "Action.Anchor";

const ALink = forwardRef<HTMLAnchorElement, LinkProps>(
  ({ children, ...props }, ref) => (
    <Link {...props} ref={ref}>
      {children}
    </Link>
  )
);

ALink.displayName = "Action.Link";

const AButton = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ children, ...props }, ref) => (
    <button {...props} ref={ref}>
      {children}
    </button>
  )
);

AButton.displayName = "Action.Button";

type ActionProps = ComponentPropsWithRef<
  typeof AAnchor | typeof ALink | typeof AButton
>;

export default function Action({ children, ...props }: ActionProps) {
  return "href" in props ? (
    <a {...(props as AnchorProps)}>{children}</a>
  ) : "to" in props ? (
    <Link {...(props as LinkProps)}>{children}</Link>
  ) : (
    <button {...(props as ButtonProps)}>{children}</button>
  );
}

The Implementation In Action

Okay, so we have a fancy "Button" implementation. How can we use this in a real scenario? Let's cover two very common examples:

The Button

This is your core button design. Whether you have a design system or just a few variants we all have a button component or set of styles that gets used all over our app. Here is how to use the Action implementation above to create your Button component.

// ~/components/kits/Button

import type { ComponentPropsWithRef } from "react";

import Action from "~/components/impl/Action";


type Variant = {
  variant?: keyof typeof variants;
};

const variants = {
  primary: {
    bg: "from-gray-100 to-gray-100 group-hover:from-accent group-hover:to-accent-bright group-focus:from-accent group-focus:to-accent-bright",
    text: "group-hover:text-white group-focus:text-white",
  },
  danger: {
    bg: "from-gray-100 to-gray-100 group-hover:from-danger group-hover:to-danger-hover group-focus:from-danger group-focus:to-danger-hover",
    text: "group-hover:text-white group-focus:text-white",
  },
  light: {
    bg: "from-white/25 to-white/10 group-hover:from-accent group-hover:to-accent-dark group-focus:from-accent-dark group-focus:to-accent",
    text: "",
  },
} as const;

const baseClasses =
  "absolute inset-0 translate-x-2 translate-y-2 bg-gradient-to-br transition-all group-hover:translate-x-1 group-hover:translate-y-1 disabled:opacity-50 disabled:pointer-events-none";

export default function Button({
  children,
  variant = "primary",
  className,
  ...props
}: ComponentPropsWithRef<typeof Action> & Variant) {
  const { bg, text } = variants[variant];
  return (
    <Action
      {...props}
      className={`group relative select-none focus:outline-none disabled:pointer-events-none disabled:opacity-60 ${
        className ?? ""
      }`}
    >
      <span className={`${baseClasses} ${bg ?? ""}`}></span>
      <span
        className={`relative flex items-center justify-center gap-2 border py-2 px-6 text-center ${
          text ?? ""
        }`}
      >
        {children}
      </span>
    </Action>
  );
}

The Menu Item

A very different component visually than a <Button/> , but functionally the same.

// ~/components/kits/MenuItem

const variants = {
DEFAULT: "hover:bg-layer-2",
danger: "hover:bg-danger",
} as const;

export default function MenuItem({ children, className, variant = "DEFAULT", ...props }: ComponentPropsWithRef<typeof Action> & Variant) => (
    <Action {...props} className={`${variants[variant] ?? ""} ${className ?? ""}`}>
      {children}
    </Action>
  )
);

But wait, there's more...

These two examples are just the start. The <Action/> implementation can be used to create any number of components. Here a couple you can find over in the Pattern Library:

  • ModalButton

  • FormButton

Updates and More

Get updated when new articles, products, or components are released. Also, whatever else I feel like would be fun to send out.