Complex components such as dialogs and menus can be challenging to style because they are made up of many parts. For example, a dialog might include a header, body, and footer.
Applying base styles is straightforward, but how can one create an error variant with a red header? Or override the body padding for a specific instance?
Let's explore a pattern that addresses these questions and improves the developer experience at the same time.
Separation of style and state
Before we get started, let’s agree on a few basic principles:
Components should expose only one prop for style overrides. Avoid creating separate props for each part, such as bodyStyles or hasPadding. This practice adds unnecessary complexity and makes it difficult to iterate on components.
Customizable parts should be easy to find and consistent. Consumers should be able to style your component without analyzing source code. In addition to using a single prop, make sure it is typed to provide autocompletion support.
Avoid using conditional logic for managing variants. When dealing with multiple style overrides, inline conditional styling can get messy quickly. It's cleaner to define styles as configuration and use an abstraction to handle the logic.
Extend your component props with VariantProps, using typeof the object created in the previous step. This will add any variants as props, as well as the classNames prop.
It only takes a small amount of setup to combine TypeScript and Tailwind to style complex components.
Here at Polytomic we’ve found it beneficial to use Tailwind Variants even for smaller components. It’s a simple pattern that promotes a clear separation of concerns.