UI Components

A collection of handy-dandy UI components for enhancing the functionality of some existing UI libraries out there (react-bootstrap, @headlessui, etc) without having to write all of the extra logic. One would argue that many of these enhancements should be baked into these default libraries - but keep in mind it's also a good idea to keep libraries lightweight - and customizable. This is a selection that I've either written from scratch, or wrapped components from libraries to make them more usable. Any required dependencies are noted.

React-Bootstrap Alert

Better React-Boostrap alert with icon support, internal dismissing, fixed functionality.

Requires: react-bootstrap, classnames

1import classNames from 'classnames';
2import React, { CSSProperties, memo, ReactElement, ReactNode, useState } from 'react';
3import { Alert, AlertProps } from 'react-bootstrap';
4
5export interface RPCAlertProps extends AlertProps {
6  icon?: ReactNode;
7  isFixed?: boolean;
8  offsetTop?: number;
9}
10
11const AlertComponent = (props: RPCAlertProps): ReactElement => {
12  const {
13    className,
14    children,
15    dismissible,
16    icon,
17    isFixed,
18    offsetTop,
19    onClose,
20    show,
21    variant,
22  } = props;
23
24  const [isAlertShowing, setIsAlertShowing] = useState(show);
25
26  const alertClassNames = classNames({
27    'rpc-alert border-0 rounded-0': true,
28    'rpc-alert--fixed mb-0 w-100 fixed-top': isFixed,
29    [`${className}`]: className,
30  });
31
32  const alertStyles: CSSProperties | undefined = offsetTop
33    ? {
34        top: `${offsetTop}px`,
35      }
36    : undefined;
37
38  return (
39    <Alert
40      className={alertClassNames}
41      dismissible={dismissible}
42      onClose={() => {
43        setIsAlertShowing(false);
44        if (onClose) onClose;
45      }}
46      show={dismissible ? isAlertShowing : show}
47      style={alertStyles}
48      variant={variant}>
49      <div className="rpc-alert__content d-flex">
50        {icon ? (
51          <div className="rpc-alert__icon me-3 lh-0">{icon}</div>
52        ) : undefined}
53        <div className="rpc-alert__message">{children}</div>
54      </div>
55    </Alert>
56  );
57};
58
59export const RPCAlert = memo<RPCAlertProps>(AlertComponent);
60

React-Bootstrap Button

Better React-Boostrap button with icon support, tooltip functionality, badge support, and more.

Requires: react-bootstrap, classnames

1import React, { memo, ReactElement, ReactNode } from 'react';
2import { Button, ButtonProps } from 'react-bootstrap';
3import { Color, Placement, Variant } from 'react-bootstrap/esm/types';
4import classNames from 'classnames';
5
6// NOTE: Requires other components from this page
7import { RPCBadge } from '../RPCBadge/RPCBadge';
8import { RPCTooltip } from '../RPCTooltip/RPCTooltip';
9
10export interface RPCButtonProps extends ButtonProps {
11  badgeLabel?: string;
12  badgeTextColor?: Color;
13  badgeVariant?: Variant;
14  circular?: boolean;
15  fluid?: boolean;
16  hasTooltip?: boolean;
17  icon?: ReactNode;
18  iconPosition?: 'left' | 'right';
19  label?: string;
20  tooltipContent?: string;
21  tooltipPlacement?: Placement;
22  type?: 'button' | 'submit';
23}
24
25const RPCButtonComponent = (props: RPCButtonProps): ReactElement => {
26  const {
27    active,
28    badgeLabel,
29    badgeTextColor = 'dark',
30    badgeVariant = 'warning',
31    children,
32    circular,
33    className,
34    disabled,
35    fluid,
36    hasTooltip,
37    icon,
38    iconPosition = 'left',
39    label,
40    onClick,
41    size,
42    style,
43    title,
44    tooltipContent,
45    tooltipPlacement,
46    type,
47    variant,
48  } = props;
49
50  const buttonClassNames = classNames({
51    'has-badge': badgeLabel,
52    'has-icon': icon,
53    [`icon--${iconPosition}`]: iconPosition,
54    circular: circular,
55    'w-100': fluid,
56    [`${className}`]: className,
57  });
58
59  const renderButton = (): ReactElement => {
60    return (
61      <Button
62        active={active}
63        className={buttonClassNames}
64        disabled={disabled}
65        onClick={onClick}
66        size={size}
67        style={style}
68        title={title}
69        type={type}
70        variant={variant}>
71        {label || icon || badgeLabel ? (
72          <>
73            {icon && iconPosition === 'left' ? icon : undefined}
74            {label ? <span className="btn__label">{label}</span> : undefined}
75            {badgeLabel ? (
76              <RPCBadge bg={badgeVariant} text={badgeTextColor}>
77                {badgeLabel}
78              </RPCBadge>
79            ) : undefined}
80            {icon && iconPosition === 'right' ? icon : undefined}
81          </>
82        ) : (
83          <>{children}</>
84        )}
85      </Button>
86    );
87  };
88
89  return hasTooltip ? (
90    <RPCTooltip tooltipPopper={renderButton()} placement={tooltipPlacement}>
91      {tooltipContent}
92    </RPCTooltip>
93  ) : (
94    renderButton()
95  );
96};
97
98export const RPCButton = memo<RPCButtonProps>(RPCButtonComponent);
99

React-Bootstrap Confirmation Modal

Simple confirm dialog using the default Modal component.

Requires: react-bootstrap, classnames

1import React, { memo, ReactElement, ReactNode } from 'react';
2import { Variant } from 'react-bootstrap/esm/types';
3import { ModalProps } from 'react-bootstrap';
4
5// NOTE: Requires other components from this page
6import { RPCModal } from '../RPCModal/RPCModal';
7import { RPCButton } from '../RPCButton/RPCButton';
8
9// NOTE: This exists in RPCModal - remove and import from there
10export interface RPCModalProps extends ModalProps {
11  dismissible?: boolean;
12  footer?: ReactNode;
13  title?: string | ReactNode;
14  variant?: Variant;
15}
16
17export interface RPCConfirmationModalProps extends RPCModalProps {
18  cancelLabel?: string;
19  children?: ReactNode;
20  confirmLabel?: string;
21  confirmVariant?: Variant;
22  onCancel?: () => void;
23  onConfirm?: () => void;
24}
25
26const RPCConfirmationModalComponent = (
27  props: RPCConfirmationModalProps,
28): ReactElement => {
29  const {
30    backdrop,
31    cancelLabel = 'Cancel',
32    children,
33    confirmLabel = 'Confirm',
34    confirmVariant = 'danger',
35    keyboard,
36    onCancel,
37    onConfirm,
38    title = 'Are you sure?',
39    show,
40  } = props;
41
42  return (
43    <RPCModal
44      backdrop={backdrop}
45      className="confirmation-modal"
46      variant="danger"
47      footer={
48        <div className="d-flex justify-content-center align-items-center w-100">
49          <RPCButton
50            className="me-2 w-100"
51            label={confirmLabel}
52            onClick={onConfirm}
53            variant={confirmVariant}
54          />
55          <RPCButton className="w-100" label={cancelLabel} onClick={onCancel} />
56        </div>
57      }
58      keyboard={keyboard}
59      size="md"
60      title={title}
61      show={show}
62      onHide={onCancel}>
63      {children}
64    </RPCModal>
65  );
66};
67
68export const RPCConfirmationModal = memo<RPCConfirmationModalProps>(
69  RPCConfirmationModalComponent,
70);
71

React-Bootstrap Modal

Better React-Bootstrap Modal with variants.

Requires: react-bootstrap, classnames

1import classNames from 'classnames';
2import React, { memo, ReactElement, ReactNode } from 'react';
3import { Modal, ModalProps } from 'react-bootstrap';
4import { Variant } from 'react-bootstrap/esm/types';
5
6// types
7export interface RPCModalProps extends ModalProps {
8  dismissible?: boolean;
9  footer?: ReactNode;
10  title?: string | ReactNode;
11  variant?: Variant;
12}
13
14// util
15const isLightText = (variant: Variant): boolean => {
16  const textLightVariants = ['black', 'dark', 'danger', 'primary', 'info'];
17  return textLightVariants.includes(variant);
18};
19
20const ModalComponent = (props: RPCModalProps): ReactElement => {
21  const {
22    backdrop,
23    children,
24    centered = true,
25    className,
26    closeVariant,
27    dismissible,
28    footer,
29    fullscreen,
30    keyboard,
31    onHide,
32    onShow,
33    scrollable,
34    show,
35    size = 'lg',
36    title,
37    variant,
38  } = props;
39
40  const modalClassNames = classNames({
41    'rpc-modal': true,
42    [`${className}`]: className,
43  });
44
45  const modalHeaderClassNames = classNames({
46    [`bg-${variant}`]: variant,
47    [`text-light`]: variant && isLightText(variant),
48  });
49
50  return (
51    <Modal
52      backdrop={backdrop}
53      centered={centered}
54      className={modalClassNames}
55      fullscreen={fullscreen}
56      keyboard={keyboard}
57      onHide={onHide}
58      onShow={onShow}
59      scrollable={scrollable}
60      show={show}
61      size={size}>
62      {dismissible || title ? (
63        <Modal.Header
64          className={modalHeaderClassNames}
65          closeVariant={
66            closeVariant || isLightText(variant) ? 'white' : undefined
67          }
68          closeButton={dismissible}>
69          {title ? <Modal.Title>{title}</Modal.Title> : undefined}
70        </Modal.Header>
71      ) : undefined}
72      <Modal.Body>{children}</Modal.Body>
73      {footer ? <Modal.Footer>{footer}</Modal.Footer> : undefined}
74    </Modal>
75  );
76};
77
78export const RPCModal = memo<RPCModalProps>(ModalComponent);
79

React-Bootstrap Pagination

A React-Bootstrap pagination component with thresholds, variant, etc.

Requires: react-bootstrap, classnames

1import classNames from 'classnames';
2import React, { memo, ReactElement } from 'react';
3import Pagination, { PaginationProps } from 'react-bootstrap/Pagination';
4import { Variant } from 'react-bootstrap/esm/types';
5
6// types
7export interface RPCPaginationProps extends PaginationProps {
8  activePage?: number;
9  ellipsisThreshold?: number;
10  isNextDisabled?: boolean;
11  isPreviousDisabled?: boolean;
12  onEllipsisClick?: () => void;
13  onFirstClick?: () => void;
14  onLastClick?: () => void;
15  onNextClick?: () => void;
16  onPageClick?: (pageNumber: number) => void;
17  onPreviousClick?: () => void;
18  pageCount: number;
19  variant?: Variant;
20}
21
22const RPCPaginationComponent = (props: RPCPaginationProps): ReactElement => {
23  const {
24    activePage = 1,
25    ellipsisThreshold = 5,
26    isNextDisabled,
27    isPreviousDisabled,
28    onEllipsisClick,
29    onFirstClick,
30    onLastClick,
31    onNextClick,
32    onPageClick,
33    onPreviousClick,
34    pageCount,
35    size,
36    variant,
37  } = props;
38
39  const paginationClassNames = classNames({
40    'rpc-pagination': true,
41    [`rpc-pagination--${variant}`]: variant,
42  });
43
44  let isPageNumberOutOfRange: boolean;
45
46  const paginationItems = [...new Array(pageCount)].map(
47    (_, index: number): ReactElement => {
48      const pageNumber = index + 1;
49      const isPageNumberFirst = pageNumber === 1;
50      const isPageNumberLast = pageNumber === pageCount;
51      const isCurrentPageWithinTwoPageNumbers =
52        Math.abs(pageNumber - activePage) <= 2;
53
54      const showPaginationItem =
55        pageCount <= ellipsisThreshold
56          ? true
57          : isPageNumberFirst ||
58            isPageNumberLast ||
59            isCurrentPageWithinTwoPageNumbers;
60
61      if (showPaginationItem) {
62        isPageNumberOutOfRange = false;
63        return (
64          <Pagination.Item
65            key={pageNumber}
66            onClick={() => onPageClick(pageNumber)}
67            active={pageNumber === activePage}
68          >
69            {pageNumber}
70          </Pagination.Item>
71        );
72      }
73
74      if (!isPageNumberOutOfRange) {
75        isPageNumberOutOfRange = true;
76        return (
77          <Pagination.Ellipsis
78            key={pageNumber}
79            className="muted"
80            onClick={onEllipsisClick}
81          />
82        );
83      }
84
85      return null;
86    },
87  );
88
89  return (
90    <Pagination size={size} className={paginationClassNames}>
91      {onFirstClick ? <Pagination.First onClick={onFirstClick} /> : undefined}
92      <Pagination.Prev
93        onClick={onPreviousClick}
94        disabled={isPreviousDisabled}
95      />
96      {paginationItems}
97      <Pagination.Next onClick={onNextClick} disabled={isNextDisabled} />
98      {onLastClick ? <Pagination.Last onClick={onLastClick} /> : undefined}
99    </Pagination>
100  );
101};
102
103export const RPCPagination = memo<RPCPaginationProps>(RPCPaginationComponent);
104

React-Bootstrap Tooltip

A React-Bootstrap tooltip with popper, rootClose functionality.

Requires: react-bootstrap, classnames

1import classNames from 'classnames';
2import React, { memo, ReactElement, ReactNode } from 'react';
3import { OverlayTrigger, Tooltip, OverlayTriggerProps, TooltipProps } from 'react-bootstrap';
4
5// types
6export interface RPCTooltipProps
7  extends TooltipProps,
8    Omit<OverlayTriggerProps, 'children' | 'overlay'> {
9  tooltipPopper?: ReactNode;
10  transparent?: boolean;
11}
12
13const RPCTooltipComponent = (props: RPCTooltipProps): ReactElement => {
14  const {
15    children,
16    id,
17    placement = 'right',
18    tooltipPopper,
19    show,
20    transparent,
21  } = props;
22
23  const tooltipClassNames = classNames({
24    transparent: transparent,
25  });
26
27  return (
28    <OverlayTrigger
29      placement={placement}
30      // delay={{show: 250, hide: 400}}
31      overlay={
32        <Tooltip
33          id={id || `tooltip-${placement}`}
34          show={show}
35          className={tooltipClassNames}>
36          {children}
37        </Tooltip>
38      }
39      rootClose>
40      <div className="tooltip-popper">{tooltipPopper}</div>
41    </OverlayTrigger>
42  );
43};
44
45export const RPCTooltip = memo<RPCTooltipProps>(RPCTooltipComponent);
46