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