← Back to list

An Easy Way To Create <Button>, <Input>, and <Select> Elements That Conform to A Systems Design

In this article, we describe in detail in easy way to create reusable React components (<button>, <input>, and <select> elements) that can…

brianonchain · 2026-06-16 22:17 · 0 claps · 6.8 min read
#react #reusable-component #design-systems #ui #frontend-development
Open on Medium ↗
Wiki topics: PRD · Product Design 🌐 · Web Development

An Easy Way To Create <Button>, <Input>, and <Select> Elements That Conform to A Systems Design

In this article, we describe in detail in easy way to create reusable React components (<button>, <input>, and <select> elements) that can conform to any System Design.

<Button>

import Spinner from "./Spinner";

const variants = {
  primary: "buttonPrimaryColorGlass",
  outline:
    "desktop:hover:bg-buttonOutlineBgHover active:bg-buttonOutlineBgHover border border-buttonOutlineBorder [transition:background-color_300ms]",
  danger: "buttonDangerColorGlass",
  dangerOutline:
    "text-textDanger desktop:hover:bg-buttonOutlineBgHover active:bg-buttonOutlineBgHover border border-buttonOutlineBorder [transition:background-color_300ms]",
  input: "justify-start inputPrimaryColor font-normal",
  // custom variants
  keypad: "keypadColor",
} as const;

const sizes = {
  xs: "h-11 desktop:h-8 px-3 desktop:px-2.5 textSm rounded-lg font-medium",
  sm: "h-12 desktop:h-9 px-3 desktop:px-2.5 rounded-lg font-medium",
  base: "h-13 desktop:h-10 px-4 desktop:px-3 rounded-lg font-medium",
  login: "h-13 desktop:h-12 px-4 desktop:px-3.5 rounded-xl font-medium",
  // custom sizes
  icon: "flex-none size-9 desktop:size-8 rounded-lg",
  pill: "h-11 desktop:h-8 px-4 desktop:px-3.5 textXs rounded-full gap-2 font-normal",
  keypad: "w-20 h-20 desktop:w-12 desktop:h-12 textXl font-semibold rounded-full",
} as const;

type ButtonProps = {
  label?: React.ReactNode;
  variant?: keyof typeof variants;
  size?: keyof typeof sizes;
  isLoading?: boolean;
  icon?: React.ReactNode;
  iconRight?: React.ReactNode;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;

export default function Button({
  label = "",
  variant = "primary",
  size = "base",
  isLoading = false,
  icon,
  iconRight,
  // destructured button props
  className = "",
  disabled,
  type = "button",
  ...props
}: ButtonProps) {
  return (
    <button
      {...props}
      className={`flex items-center justify-center gap-1 disabled:cursor-default select-none ${variants[variant]} ${sizes[size]} ${className}`}
      disabled={isLoading || disabled}
      type={type}
    >
      {isLoading ? (
        <Spinner buttonVariant={variant} />
      ) : (
        <>
          {icon}
          {label}
          {iconRight}
        </>
      )}
    </button>
  );
}
  1. variant and size props should adhere to the System Design. Typical button variants include primary, secondary, outline, ghost, and danger. For sizes, base is the standard size. For desktop, the Login Page text and button size are usually larger than in the app (for mobile, they are the same). Sizes xs and sm are used when the content is more compact.
  2. We define variant and size as const and use keyof typeof <const> to define ButtonProps. This way, we can add new variants or sizes without re-editing ButtonProps (to me, this is fairly convenient).
  3. Width is not included inclassName, so the default button collapses in width to the pre-defined x padding. className="w-full" should be used for <Button>’s that spans the page’s content (i.e., within modals).
  4. icon and iconRight allows for icons on the left (default) or right of the text.
  5. When isLoading is true, all text/icons are replaced with a <Spinner>. buttonVariant is used to define the spinner color.
  6. disabled, type, and className are destructured from ButtonHTMLAttributes to allow us to 1) define the disabled state with isLoading, 2) set default type to button, and 3) add custom classes to the button.

<Input>

const variants = {
  primary: "inputPrimaryColor",
  danger: "inputDangerColor",
  login: "inputLoginColor", // only used for login form
} as const;

const sizes = {
  xs: "h-11 desktop:h-8 px-3 desktop:px-2.5 textSm rounded-lg", // used in DetailsModal.tsx
  sm: "h-12 desktop:h-9 px-3 desktop:px-2.5 rounded-lg", // not used anywhere yet
  base: "h-13 desktop:h-10 px-3.5 desktop:px-3 rounded-lg",
  login: "h-13 desktop:h-12 px-3.5 desktop:px-3.5 rounded-xl", // login form uses 16px text for desktop
} as const;

type InputProps = {
  inputSize?: keyof typeof sizes;
  variant?: keyof typeof variants;
} & React.InputHTMLAttributes<HTMLInputElement>;

export default function Input({ inputSize = "base", variant = "primary", className = "", ...props }: InputProps) {
  return <input className={`${variants[variant]} ${sizes[inputSize]} ${className}`} {...props} />;
}
  1. Like <Button>, variant and inputSize are used to conform to the UI Systems Design. They are also defined as a const and keyof typeof <const> is used to define the InputProps (so custom variants/sizes can be added without needing to re-edit the InputProps). inputSize is used instead because InputHTMLAttributes contain already has a prop named size.
  2. In globals.css, inputPrimaryColor, inputDangerColor, etc. are defined. For example:
@utility inputPrimaryRing {
  @apply ring-2 ring-transparent focus:ring-inputPrimaryRing;
}
.inputPrimaryColor {
  @apply bg-inputPrimaryBg placeholder:text-inputPrimaryPlaceholder border border-inputPrimaryBorder desktop:hover:border-inputPrimaryBorderHover focus:border-inputPrimaryBorderHover inputPrimaryRing transition-all duration-300;
}
.inputLoginColor {
  @apply bg-inputPrimaryBg placeholder:text-inputPrimaryPlaceholder border border-inputLoginBorder desktop:hover:border-inputPrimaryBorderHover focus:border-inputPrimaryBorderHover inputPrimaryRing transition-all duration-300;
}
.inputDangerColor {
  @apply bg-inputPrimaryBg placeholder:text-inputPrimaryPlaceholder border border-textDanger desktop:hover:border-textDanger focus:border-textDanger inputDangerRing transition-all duration-300;
}

<Select>

import { FaChevronDown } from "react-icons/fa";

const variants = {
  primary: "inputPrimaryColor",
  outline: "selectOutlineColor",
} as const;

const sizes = {
  xxs: "h-9 desktop:h-8 pl-3 desktop:pl-2.5 pr-6 desktop:pr-5 textXs rounded-lg",
  xs: "h-11 desktop:h-8 pl-3 desktop:pl-2.5 pr-8 desktop:pr-6.5 textSm rounded-lg", // used in DetailsModal.tsx
  sm: "h-12 desktop:h-9 pl-3 desktop:pl-2.5 pr-8 desktop:pr-6.5 rounded-lg",
  base: "h-13 desktop:h-10 pl-3.5 desktop:pl-3 pr-9 desktop:pr-7.5 rounded-lg",
} as const;

const iconSizes = {
  xxs: "right-3 desktop:right-2.5 text-xs desktop:text-[0.625rem]",
  xs: "right-3 desktop:right-2.5 text-sm desktop:text-[0.625rem]", // used in DetailsModal.tsx
  sm: "right-3 desktop:right-2.5 text-sm desktop:text-[0.625rem]",
  base: "right-3.5 desktop:right-3 text-sm desktop:text-[0.625rem]",
} as const;

type SelectProps = {
  children: React.ReactNode;
  variant?: keyof typeof variants;
  selectSize?: keyof typeof sizes;
} & React.SelectHTMLAttributes<HTMLSelectElement>;

export default function Select({ children, variant = "primary", selectSize = "base", className = "", ...props }: SelectProps) {
  return (
    <div className={`relative ${className}`}>
      <select className={`appearance-none w-full ${variants[variant]} ${sizes[selectSize]}`} {...props}>
        {children}
      </select>
      <FaChevronDown className={`absolute top-1/2 -translate-y-1/2 opacity-80 pointer-events-none ${iconSizes[selectSize]}`} />
    </div>
  );
}
  1. Like <Button> and <Input>, variant and size are defined by the Systems Design. iconSizes defines the position and size of the dropdown icon (down chevron).

Other Notes

Systems Design Colors

In globals.css, your @theme{…} and .dark{…} should define the System Design colors. For example:

@theme {
  /* bg main */
  --color-bgPrimary: #f7f3f9;
  --color-card: #ffffff;
  --color-modal: #f7f3f9;
  /* borders */
  --color-borderFaint: oklch(92.9% 0.013 255.508); /* slate-200 */
  /* text */
  --color-textPrimary: #142634;
  --color-textSecondary: #647987;
  --color-textTertiary: #8f9faa;
  --color-link: #007cc5; /* 6% darker than buttonPrimaryBg */
  --color-linkHover: #006dab; /* 12% darker than link */
  --color-textDanger: #ff4774; /* 5% brighter than buttonDangerBg */
  --color-textDangerHover: #df2553; /* 12% darker than textDanger */
  /* buttons */
  --color-buttonPrimaryText: #ffffff;
  --color-buttonPrimaryBg: #0084d1; /* sky-600 */
  --color-buttonPrimaryBgHover: #0074b8; /* 12% darker than buttonPrimaryBg */
  --color-buttonOutlineBorder: #d6dee9; /* slate-250 */
  --color-buttonOutlineBgHoverSubtle: #f6f9fc; /* color is between bgPrimary and buttonOutlineBgHover */
  --color-buttonOutlineBgHover: #f7f3f9; /* same as bgPrimary */
  --color-buttonDangerBg: #ff3264;
  --color-buttonDangerBgHover: #e62a58; /* 8% darker than buttonDangerBg */
  --color-buttonRing: rgb(0 132 209 / 70%);
  /* input */
  --color-inputPrimaryBg: #ffffff;
  --color-inputPrimaryBorder: oklch(92.9% 0.013 255.508); /* slate-200, 4% darker than bgPrimary */
  --color-inputPrimaryBorderHover: rgb(0 132 209 / 70%);
  --color-inputPrimaryRing: rgb(0 132 209 / 40%);
  --color-inputPrimaryPlaceholder: var(--color-textTertiary);
  --color-inputLoginBorder: #adbcce; /* slate-350 */
  --color-inputDangerRing: rgb(240 68 111 / 50%);
  /* toggle */
  --color-toggleOn: var(--color-buttonPrimaryBg);
  --color-toggleOff: oklch(86.9% 0.022 252.894); /* slate-300 */
  /* custom colors */
  --color-closeButtonBgHover: #dbe1e9; /* slate-250 */
  --color-loginButtonBg: #ffffff;
  --color-loginButtonBgHover: #ffffff;
  --color-loginButtonBorder: transparent; /* slate-200 */
  --color-loginButtonBorderHover: transparent; /* slate-300 */
  /* buttons glass */
  --color-buttonPrimaryGlassBg: #0084d1;
  --color-buttonPrimaryGlassBgHover: #0074b8;
  --color-buttonPrimaryGlassBorder: transparent;
  --color-buttonDangerGlassBg: #ff3264;
  --color-buttonDangerGlassBgHover: #e62a58;
  --color-buttonDangerGlassBorder: transparent;
  /* breakpoints */
  --breakpoint-xs: 480px;
  --breakpoint-sm: 600px;
  --breakpoint-md: 750px;
  --breakpoint-lg: 980px;
  --breakpoint-xl: 1250px;
  --breakpoint-2xl: 1440px;
}

.dark {
  /* bg main */
  --color-bgPrimary: #0a0826; /* was 0b0f37*/
  --color-card: oklch(70.7% 0.165 254.624 / 7%); /* blue-400/7 */
  --color-modal: #141344; /* blue-400/14 overlaid on bgPrimary */
  /* borders */
  --color-borderFaint: oklch(70.7% 0.165 254.624 / 8%); /* blue-400/8 */
  /* text */
  --color-textPrimary: rgb(240 242 250);
  --color-textSecondary: rgb(240 242 250 / 0.7);
  --color-textTertiary: rgb(240 242 250 / 0.45);
  --color-link: #2072f6; /* 6% brighter than buttonPrimaryBg */
  --color-linkHover: #4085f5; /* 9% brighter than link */
  --color-textDanger: #ff507a;
  --color-textDangerHover: #ff7698; /* 11% brighter than textDanger */
  /* buttons */
  --color-buttonPrimaryText: #ffffff;
  --color-buttonPrimaryBg: #0364ff;
  --color-buttonPrimaryBgHover: #1c73ff; /* 8% brighter than buttonPrimaryBg */
  --color-buttonOutlineBorder: #293b70;
  --color-buttonOutlineBgHoverSubtle: oklch(70.7% 0.165 254.624 / 7%); /*  blue-400/7 */
  --color-buttonOutlineBgHover: oklch(70.7% 0.165 254.624 / 14%); /* blue-400/14 */
  --color-buttonDangerBg: #ff3a6a;
  --color-buttonDangerBgHover: #ff507a; /* 6% brighter than buttonDangerBg */
  --color-buttonRing: #546bb0;
  /* inputs */
  --color-inputPrimaryBg: #0a0826; /* bgPrimary */
  --color-inputPrimaryBorder: #131f40;
  --color-inputPrimaryBorderHover: #314278;
  --color-inputPrimaryRing: #1e2e60;
  --color-inputLoginBorder: var(--color-inputPrimaryBorder);
  --color-inputDangerRing: rgb(255 80 122 / 60%);
  /* toggle */
  --color-toggleOn: #0364ff;
  --color-toggleOff: rgb(240 242 250 / 0.2);
  /* custom colors */
  --color-closeButtonBgHover: oklch(70.7% 0.165 254.624 / 14%);
  --color-loginButtonBg: transparent;
  --color-loginButtonBgHover: oklch(70.7% 0.165 254.624 / 7%);
  --color-loginButtonBorder: rgb(255 255 255 / 21%);
  --color-loginButtonBorderHover: rgb(255 255 255 / 21%);
  /* buttons glass */
  --color-buttonPrimaryGlassBg: rgba(3, 100, 255, 0.5);
  --color-buttonPrimaryGlassBgHover: rgba(3, 100, 255, 0.65);
  --color-buttonPrimaryGlassBorder: rgba(73, 138, 244, 0.5);
  --color-buttonDangerGlassBg: rgba(255, 58, 106, 0.75);
  --color-buttonDangerGlassBgHover: rgba(255, 58, 106, 0.85);
  --color-buttonDangerGlassBorder: rgba(255, 112, 143, 0.75);
}

Supplemental Base Styles

I also added these base styles to globals.css:

button {
  cursor: pointer;
}
@utility scrollbar-stable {
  scrollbar-gutter: stable;
}
:focus-visible {
  outline: 2px solid var(--color-buttonRing);
  outline-offset: 2px;
}
/* Inputs/selects use ring/border instead of global outline */
:where(input, select, textarea):focus-visible {
  outline: none;
}
/* --- add this to buttons within scroll containers where outline overflows and becomes clipped (e.g., item list or details list) ---*/
.innerOutline:focus-visible {
  outline-offset: -2px;
}
  • all buttons will have cursor-pointer
  • all elements with :focus-visble (except for input, select, and textarea; thus, mainly <a> and <button>) will have an outline with +2px offset. This is needed for users who use “tabs” for navigation. The offset is needed for better visibility, as most buttons have a solid color.
  • input, select, and textarea will use ring-2 (no offset) when focused. Having zero offset is more natural for these elements. These elements will have a highlighted border when focused, so a ring-2 with a softer color/glow (and transparency) will be used.
  • innerOutline class is added to <Button> whenever a +2px outline will be cut off. For example, clickable items in a list or segmented buttons will need this class.

Summary

After some experimentation, I found the above to be the easiest way (least cognitive load) to create button, input, and select elements that conform to a Systems Design.


메타데이터
post_id
732db49abfd0
slug
creating-button-input-and-select-elements-that-conform-to-a-systems-design-732db49abfd0
url
https://medium.com/@brianonchain/creating-button-input-and-select-elements-that-conform-to-a-systems-design-732db49abfd0
canonical_url
https://medium.com/@brianonchain/creating-button-input-and-select-elements-that-conform-to-a-systems-design-732db49abfd0
author_url
https://medium.com/@brianonchain
status
ok
fetched_at
2026-06-17 17:19:58