DEV Community

Cover image for DIY Component Library Trap: Opinionated Design Systems
Ridwan Ajibola
Ridwan Ajibola

Posted on

1 1

DIY Component Library Trap: Opinionated Design Systems

In my experience developing component libraries for various startups, I've noticed a recurring pattern. Time and again, teams try to break free from opinionated libraries like MUI, AntDesign, or ShadCN by building custom design systems—only to find their custom systems becoming just as rigid and limited in scope. Even when using utility-first frameworks like Tailwind CSS, if overused or misapplied, excessive prop passing forces your custom library into inflexible, project-specific patterns that can’t be easily reused elsewhere.

The core issue is that when you try to escape established opinions without a modular strategy, you end up recreating them in a form tailored to a specific project rather than being reusable across multiple projects. My experience and community feedback suggest the best solution for React is a hybrid, localized styling approach. This approach works with any CSS strategy—CSS Modules, SCSS, CSS-in-JS (like twin.macro), or even Tailwind—while keeping components reusable across projects.


The Problem: Complex & Opinionated Libraries

Popular component libraries like MUI, AntDesign, and ShadCN guide you toward a specific design language, which can sometimes limit your ability to create a truly unique look and reuse components across projects. Meanwhile, if not managed properly, even a utility-first approach (like Tailwind CSS) can lead to excessive prop passing, resulting in a custom component library that is as opinionated as the established ones and often limited to just one or two projects.

In practice, teams that try to “escape” these opinions often end up with:

  • Rigid Patterns: Pre-defined design tokens and utilities enforce uniformity that may not suit your product’s unique identity—and these patterns are so tailored to one project that they hinder reuse in others.
  • Excessive Prop Passing: Relying solely on utility-first approaches can result in lengthy lists of props and class overrides. For example, a button with left and right icons might require individual style props just to tweak margins, paddings, colors, and hover effects—locking the component into a project-specific setup.
  • Reinventing the Wheel: In an effort to break free from established libraries, teams build custom component libraries that, without a modular strategy, inherit rigid, project-specific patterns. This not only makes them difficult to maintain but also limits their reusability across different projects.

The CSS Custom Properties Strategy for Flexible Component Libraries

The solution to building flexible, reusable components lies in leveraging CSS custom properties to create clear customization points while keeping styling choices open. This approach ensures your components remain adaptable across different projects, regardless of your team's preferred styling solution.

Let's explore this strategy through a practical example: a Button component that needs to support icons, variants, and project-specific styling requirements.

Core Component Architecture

The foundation starts with a base Button component that uses CSS custom properties for key styling points:

const Button = ({
  variant = "default",
  icon,
  children,
  className,
  ...rest
}) => (
  <button
    className={`button ${variant} ${className}`}
    {...rest}
  >
    {icon && <IconContainer>{icon}</IconContainer>}
    <span>{children}</span>
  </button>
);

const IconContainer = styled.div`
  height: var(--icon-container-size, 16px);
  width: var(--icon-container-size, 16px);
  padding: var(--icon-container-padding, 0);
  margin: var(--icon-container-margin, 0 4px 0 0);

  svg {
    padding: var(--icon-padding, 2px);
    color: var(--icon-color, currentColor);
  }
`;
Enter fullscreen mode Exit fullscreen mode

The base Button component should also adhere to accessibility best practices, such as appropriate ARIA attributes for screen readers.

This structure provides several key advantages:

  • Clear Customization Points: CSS custom properties define exactly what can be customized.
  • Default Styling: Base styles with sensible defaults.
  • Flexibility: The className prop allows for easy style overrides using any styling approach.

Implementation Options

The beauty of this approach is its flexibility in implementation. Teams can choose their preferred styling solution while maintaining component consistency:

  • Using CSS Modules:
// TextButton.module.scss

.textButton {
  --icon-container-size: 20px;
  --icon-padding: 4px;

  background: transparent;
  color: var(--color-text-primary);

  &:hover {
    background: var(--color-background-hover);
  }
}
Enter fullscreen mode Exit fullscreen mode
import styles from './TextButton.module.scss';

const TextButton = (props) => (
  <Button className={styles.textButton} {...props} />
);
Enter fullscreen mode Exit fullscreen mode
  • Using Styled Components:
const TextButton = styled(Button)`
  --icon-container-size: 20px;
  --icon-padding: 4px;

  background: transparent;
  color: ${props => props.theme.colors.primary};

  &:hover {
    background: ${props => props.theme.colors.backgroundHover};
  }
`;

// Usage
<TextButton
  variant="outline"
  icon={<Icon name="Info" />}
>
  Learn More
</TextButton>
Enter fullscreen mode Exit fullscreen mode
  • Using SCSS
.text-button {
  --icon-container-size: 20px;
  --icon-padding: 4px;

  @include button-base;

  background: transparent;

  &:hover {
    background: $color-background-hover;
  }
}
Enter fullscreen mode Exit fullscreen mode
  • Optional: Tailwind Integration For teams using Tailwind, the component can easily integrate with utility classes:
<Button
  className="bg-transparent hover:bg-gray-50"
  style={{
    '--icon-container-size': '20px',
    '--icon-padding': '4px'
  }}
  icon={<Icon name="Info" />}
>
  Learn More
</Button>
Enter fullscreen mode Exit fullscreen mode

Real-World Effectiveness

This strategy has proven highly effective in real-world applications for several reasons:

True Component Reusability: Instead of creating rigid, project-specific components, this approach allows teams to:

  • Maintain consistent component structure.
  • Customize styling for specific project needs.
  • Avoid rebuilding components for each new project.

Reduced Prop Complexity: Rather than passing numerous style props, we can:

  • Use CSS custom properties for style customization.
  • Keep component APIs clean and focused.
  • Allow styling overrides through standard CSS mechanisms.

Flexible Implementation: Teams can:

  • Choose their preferred styling approach.
  • Mix and match styling solutions as needed.
  • Maintain consistent component behavior across different styling implementations.

Example: Building a Product Suite

Here's how this approach works in practice. Imagine building a suite of products that share common components but need distinct visual identities:

// Product A - Marketing Site
const MarketingButton = styled(Button)`
  --icon-container-size: 24px;
  --icon-container-margin: 0 8px 0 0;
  background: linear-gradient(to right, var(--color-primary), var(--color-secondary));
  color: white;
`;

// Product B - Admin Dashboard
const AdminButton = styled(Button)`
  --icon-container-size: 18px;
  --icon-padding: 2px;
  background: var(--color-background-subtle);
  color: var(--color-text-primary);
`;
Enter fullscreen mode Exit fullscreen mode

Conclusion

CSS custom properties provide a powerful solution to the component library challenge. Instead of fighting against established libraries or creating rigid custom solutions, this approach:

  • Maintains component flexibility while providing consistent structure.
  • Supports multiple styling approaches to suit team preferences.
  • Enables true component reusability across different projects.
  • Reduces the complexity of style customization.

This strategy has helped numerous teams break free from the cycle of rebuilding components for each project while maintaining the flexibility needed for unique design requirements. Whether you're building a single product or a suite of applications, this approach provides a sustainable path forward for component library development.

What challenges have you faced with component libraries in your projects? How do you balance consistency with flexibility in your design systems? Share your experiences in the comments below.

Don't forget to like, share and also connect with me on LinkedIn.

Sentry blog image

How I fixed 20 seconds of lag for every user in just 20 minutes.

Our AI agent was running 10-20 seconds slower than it should, impacting both our own developers and our early adopters. See how I used Sentry Profiling to fix it in record time.

Read more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs