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.
🧮 play around with the component
🌱 starting with a basic interface for component props
here i am using the native type for button components as a extension to the component props
export interface ButtonProps extends React.ButtonHTMLAtrributes<HTMLButtonElement> {}tsx
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; }tsx
for icons, i am using tabler icons in this example.
🎨 Writing styles for individual variants and sizes
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)); }tsx
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', }, };tsx
🧱 writing the main react component
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';text
✨ ending notes
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!