with the high adoption of tools such as next-js, shadcn-ui, tailwindcss and framer-motion, there are so many coding patterns coming in picture to follow. some focus more on the accessibility part of things and some are more on the design and aesthetic end. in such cases, following a single coding pattern becomes tough, while making sure that your code is accessible, aesthetically pleasing with all the CSS and animation tinkering.
according to my learnings and the components i have been building, i have been following the shared coding pattern for writing react components. this uses basic typescript utilities for making components with better DX and making sure they are native.
for this explanation, let's take an example of a button component, with some initial set of variants and sizes.
the button component will contain all the native-button properties. following we will add the listed properties:
variant - primary, secondary, ghost etc.
size - sm, md, lg
rightIcon - prop to render an icon in the right slot
leftIcon - prop to render an icon in the right slot
stretch - to cover the full width inside a container, relatively.
isLoading - to show loader icon on API calls & dummy loading states.
here i am using the native type for button components as a extension to the component props
export interface ButtonProps extends React.ButtonHTMLAtrributes<HTMLButtonElement> {}to the same interface, we can add custom properties using individual types
export type ButtonVariant = 'primary' | 'secondary' | 'ghost';
export type ButtonSize = 'sm' | 'md' | 'lg';
export interface ButtonProps extends React.ButtonHTMLAtrributes<HTMLButtonElement> {
variant?: ButtonVariant;
size?: ButtonSize;
isLoading?: boolean;
rightIcon?: TablerIcon;
leftIcon?: TablerIcon;
stretch?: boolean;
}for icons, i am using tabler icons in this example.
create a utility function to manage our class names using the clsx and tailwind-merge packages. this helps us handle class name conflicts and conditional styling:
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}now, let's define our button styles using tailwind classes. we'll create an object to store our variant and size mappings:
const buttonVariants = {
base: [
'inline-flex',
'items-center',
'justify-center',
'rounded-md',
'font-medium',
'border',
'transition-colors',
'focus-visible:outline-none',
'focus-visible:ring-2',
'focus-visible:ring-offset-2',
'disabled:pointer-events-none',
'disabled:opacity-50',
].join(' '),
variant: {
primary: 'bg-blue-500 text-white hover:bg-blue-600 border-transparent',
secondary: 'bg-white text-black hover:bg-gray-100',
ghost: 'hover:bg-gray-100 border-transparent',
},
size: {
sm: 'h-9 px-3 text-sm',
md: 'h-10 px-4 text-sm',
lg: 'h-11 px-8 text-base',
},
};now as the interface and types are prepared, we will write a single component for button using forwardRef, we'll also add handling for our variants, sizes, and icons:
import React from 'react';
import { Loader2 } from 'tabler-icons-react';
import { cn } from '@/lib/utils';
import { buttonVariants } from './styles';
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
(
{
className,
variant = 'primary',
size = 'md',
isLoading = false,
leftIcon: LeftIcon,
rightIcon: RightIcon,
stretch = false,
children,
disabled,
...props
},
ref
) => {
return (
<button
ref={ref}
disabled={disabled || isLoading}
className={cn(
buttonVariants.base,
buttonVariants.variant[variant],
buttonVariants.size[size],
stretch && 'w-full',
className
)}
{...props}
>
{isLoading ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
LeftIcon && (
<LeftIcon
className={cn('h-4 w-4', children ? 'mr-2' : '')}
aria-hidden="true"
/>
)
)}
{children}
{!isLoading && RightIcon && (
<RightIcon
className={cn('h-4 w-4', children ? 'ml-2' : '')}
aria-hidden="true"
/>
)}
</button>
);
}
);
Button.displayName = 'Button';remember to always consider accessibility when building components. this includes proper aria labels, keyboard navigation, and focus states - which we've included in our base styles. you can extend this pattern to create other components like input fields, select dropdowns, and more complex UI elements while maintaining the same structure and consistency.
keep building!
read the same article with live example at https://www.yashsehgal.com/writing/extensive-react-components

Comments
Nothing yet. Say the first thing.
Sign in to join the conversation.