As frontend developers, one of the easiest mistakes we make early on is writing messy, inconsistent code, especially when it comes to styling. Between utility-first frameworks like Tailwind, component libraries like Ant Design, and old standbys like Bootstrap, it's easy to get carried away mixing tools or reinventing styles that already exist.
The key to writing clean, maintainable, and reusable code is discipline. Below are four fundamental rules that can help new frontend developers stay consistent and avoid common pitfalls.
Rule 1: Stick to One Library π―
If you've chosen a UI library, whether it's Bootstrap, Tailwind CSS, Shadcn (which builds on Tailwind), or Ant Design, commit to it.
Why This Matters:
- Mixing multiple libraries creates unnecessary bloat and inconsistencies
- A single library ensures your design system is predictable and your project remains easy for others to read and maintain
- Most modern libraries are powerful enough to handle 90% of your use cases, so learn their feature set thoroughly before bringing in alternatives
Implementation:
- Choose one styling framework as your primary solution
- Resist the temptation to mix multiple frameworks unless absolutely necessary
- Maintain consistency across your entire project
π‘ Tip: Explore the documentation deeply. Chances are, the utility or component you're about to write already exists in your chosen library.
Rule 2: Exhaust the Utility Classes Before Writing Custom Styles π§
Many beginners jump straight into writing new CSS rules for simple tweaks like spacing, typography, or colors. This leads to duplicated styles and harder-to-maintain code.
Instead, trust the utility classes your library provides.
Benefits:
- Rapid Development: Pre-built utilities speed up styling
- Design System Consistency: Utilities enforce design constraints
- Responsive Design: Most utilities include responsive variants
- Reduced CSS Bloat: No duplicate or conflicting custom styles
Example:
Instead of writing:
β Bad:
.tiny-link {
font-size: 12px;
font-weight: bold;
}
<a class="tiny-link">Read more</a>
β
In Tailwind, youβd just use:
<a class="text-xs font-bold">Read more</a>
Using utilities keeps everything consistent, semantic, and maintainable.
Rule 3: Trust the Library Before Adding Custom Props or Overrides π€
Libraries and HTML elements already provide sensible defaults and properties. Before you override them with custom props or inline styles, see if the library can handle it first.
Key Principles:
- Use library-provided components and their intended APIs
- Prefer library's sizing, spacing, and layout systems
- Utilize built-in responsive breakpoints and design tokens
- Add custom modifications only when library solutions are insufficient
Example 1: Button Component
β Bad - Custom props that fight the system:
interface BadButtonProps {
children: React.ReactNode;
width?: number;
height?: number;
backgroundColor?: string;
textColor?: string;
borderRadius?: number;
fontSize?: number;
padding?: string;
onClick?: () => void;
}
const BadButton: React.FC<BadButtonProps> = ({
children,
width = 120,
height = 40,
backgroundColor = '#3b82f6',
textColor = '#ffffff',
borderRadius = 8,
fontSize = 14,
padding = '8px 16px',
onClick
}) => {
return (
<button
onClick={onClick}
style={{
width: `${width}px`,
height: `${height}px`,
backgroundColor,
color: textColor,
borderRadius: `${borderRadius}px`,
fontSize: `${fontSize}px`,
padding,
border: 'none',
cursor: 'pointer'
}}
>
{children}
</button>
);
};
// Usage creates inconsistency
<BadButton width={200} height={50} backgroundColor="#ef4444">
Delete
</BadButton>
β
Good - Extends HTML button with design system variants:
import { cn } from "@/lib/utils";
import { cva, type VariantProps } from "class-variance-authority";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 disabled:pointer-events-none",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "underline-offset-4 hover:underline text-primary"
},
size: {
default: "h-10 py-2 px-4",
sm: "h-9 px-3 rounded-md",
lg: "h-11 px-8 rounded-md",
icon: "h-10 w-10"
}
},
defaultVariants: {
variant: "default",
size: "default"
}
}
);
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
const Button: React.FC<ButtonProps> = ({
className,
variant,
size,
...props
}) => {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
};
// Usage follows design system
<Button variant="destructive" size="lg" className="w-full">
Delete Account
</Button>
Example 2: SVG Icon Component
β Bad - Custom props that bypass HTML attributes:
interface BadIconProps {
width?: number;
height?: number;
color?: string;
strokeWidth?: number;
className?: string;
}
const BadTrashIcon: React.FC<BadIconProps> = ({
width = 24,
height = 24,
color = '#000000',
strokeWidth = 2,
className
}) => {
return (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"
stroke={color}
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};
// Usage creates inconsistent styling
<BadTrashIcon width={32} height={32} color="#ef4444" strokeWidth={3} />
β
Good - Extends SVG element and uses currentColor:
import { forwardRef } from "react";
interface IconProps extends React.SVGProps<SVGSVGElement> {}
const TrashIcon = forwardRef<SVGSVGElement, IconProps>(
({ className, ...props }, ref) => {
return (
<svg
ref={ref}
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
<path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2" />
</svg>
);
}
);
TrashIcon.displayName = "TrashIcon";
// Usage works with design system classes
<TrashIcon className="h-6 w-6 text-red-500 hover:text-red-700" />
<Button variant="destructive" className="text-white">
<TrashIcon className="h-4 w-4 mr-2" />
Delete
</Button>
Why These Examples Matter:
Button Component Benefits:
- Uses design system variants instead of arbitrary props
- Extends native HTML button attributes for accessibility
- Leverages Tailwind classes for consistent sizing and spacing
- Maintains type safety with proper TypeScript interfaces
SVG Icon Benefits:
- Uses
currentColor
to inherit text color from parent - Extends all native SVG properties for maximum flexibility
- Works seamlessly with utility classes for sizing and colors
- Can be styled contextually by parent components
This approach ensures you're working with your design system instead of against it. Override only when it's absolutely necessary.
Rule 4: Keep Components Small and Focused π―
One of the best ways to keep your code reusable is to make sure your components do one thing well.
The Principle:
- A component should be focused on a single responsibility, whether that's rendering a button, a card, or a form input
- If a component grows too large or tries to do too much, split it into smaller subcomponents
- Smaller components are easier to test, reuse, and maintain
Example:
Instead of creating a single massive UserProfile
component that handles layout, fetching data, and rendering UI:
β Better approach:
- Create smaller components like
UserAvatar
,UserBio
, andUserStats
- Compose them inside
UserProfile
This makes your project easier to extend later without rewriting large chunks of code.
Why These Rules Matter π
Following these rules is essentially about discipline:
- Consistency: Your code looks the same no matter who writes it
- Maintainability: Anyone new to the project can jump in without learning multiple systems
- Reusability: Components remain generic and adaptable instead of being one-off hacks
- Performance: Avoid bloated builds from multiple frameworks
- Team Collaboration: Shared conventions make collaboration smoother
Related Principles π
These rules don't exist in isolation, they reflect established software principles:
- Single Source of Truth (SSOT): One UI library as the canonical styling source
- Don't Repeat Yourself (DRY): Avoid writing styles that already exist
- Convention over Configuration: Follow the conventions of your chosen library
- Design System Discipline: Stick to the system, extend only when needed
- Single Responsibility Principle (SRP): Each component should do one thing well
Industry Standards:
- Utility-First CSS - Popularized by Tailwind CSS
- Atomic Design by Brad Frost - Building consistent UI components
- Design Tokens - Standardized design decisions in code
- Component-Driven Development - Building UIs from reusable components
Conclusion π‘
Clean frontend code isn't about avoiding creativity, it's about using the right tools the right way. By sticking to one library, fully exploring its utilities, trusting its conventions, and keeping components small and focused, you'll write code that's not only clean and reusable but also easy for your team to maintain and scale.
Remember: discipline in following these rules early on will save you countless hours of refactoring later.
What's your biggest challenge when it comes to writing clean frontend code? Share your thoughts in the comments below! π
Top comments (0)