Share complex style configs with styled-components
Tags: webdev
Published: 2023-02-24 22:09:09

Recently on a project we worked on the new set of form components for internal library.
Our stack includes `styled-components` and I want to share an approach, which might be useful for someone. No knowledge behind basics (`styled` and `css` utilities) of aforementioned library is needed.

Let's say we have an input component, with border and border-radius configured. We assume that it can resemble complex structure, so we'll use simple div as a wrapper

```jsx
const StyledInputContainer = styled.div`
  border: 1px solid black;
  border-radius: 4px;
`

const Input = () => {
  return <StyledInputContainer>
           <input />  // Just for illustrative purposes, we won't build real Input here
         </StyledInputContainer>
}
```
Fine. What if we want to allow user to remove value by clicking on cross button? We can add it to our Input

```diff
+ const StyledClearButton = styled.button`
+   // other styles for button
+  `

const StyledInputContainer = styled.div`
  border: 1px solid black;
  border-radius: 4px;
+  display: flex;
+  justify-content: space-between;
`

const Input = () => {
  return <StyledInputContainer>
           <input />
 +         <StyledClearButton>X</StyledClearButton> 
         </StyledInputContainer>
}
```
Having button hanging all the time in the input might be distracting for user, we can show it only if container is hovered.

```diff
const StyledClearButton = styled.button`
+   visibility: hidden;
    // other styles for button
`

const StyledInputContainer = styled.div`
  border: 1px solid black;
  border-radius: 4px;
  display: flex;

+  &:hover {                     
+    ${StyledClearButton} {    // Way to create css cascade
+      visibility: visible;
+    }
+  }
`

const Input = () => {
  return <StyledInputContainer>
           <input />
           <StyledClearButton>X</StyledClearButton> 
         </StyledInputContainer>
}
```
Now the button will be visible only if container is hovered.
We can even add props to this setup. For example, if our input has errors, we want it to have red border.
```diff
const StyledClearButton = styled.button`
   visibility: hidden;
   // other styles for button
`

const StyledInputContainer = styled.div`
-  border: 1px solid black;
+  border: 1px solid ${props.error ? 'red' : 'black'};
  border-radius: 4px;
  display: flex;

  &:hover {                     
    ${StyledClearButton} {
      visibility: visible;
    }
  }
`

const Input = () => {
+ // error state is handled here
- return <StyledInputContainer>
+ return <StyledInputContainer error={error}>
           <input />
           <StyledClearButton>X</StyledClearButton> 
         </StyledInputContainer>
}
```

Until now it was pretty common development path for `styled-components` adoption. But here is the tricky part. In a very different module we want to have a `TextArea` component, which will have same border, border-radius (background color, hover/active states, etc.) as our `Input`. But it shouldn't have `ClearButton`. So the question is - how to exploit what is reusable here?
Answer is simple - put common part to the function and use it to populate different styled wrappers. We can do it first for the `Input`
```diff
const StyledClearButton = styled.button`
   visibility: hidden;
   // other styles for button
`

+ export const getCommonInputStyles = (props, closeButtonCmp) => css`
+   border: 1px solid ${props.error ? 'red' : 'black'};
+   border-radius: 4px;
+   ${closeButtonCmp && css`
+     &:hover {                     
+       ${closeButtonCmp} {
+         visibility: visible;
+       }
+     }
+   `}
+ `

const StyledInputContainer = styled.div`
-  border: 1px solid ${props.error ? 'red' : 'black'};
-  border-radius: 4px;
   display: flex;

-  &:hover {                     
-   ${StyledClearButton} {
-     visibility: visible;
-   }
- }
+  ${props => getCommonInputStyles(props, StyledClearButton)}
`

const Input = () => {
 // error state is handled here
 return <StyledInputContainer>
 return <StyledInputContainer error={error}>
           <input />
           <StyledClearButton>X</StyledClearButton> 
         </StyledInputContainer>
}
```
And now we can use extracted styles in whatever else component we want. Like mentioned above TextArea
```jsx
const StyledTextAreaContainer = styled.div`
  ${props => getCommonInputStyles(props)}
`

const TextArea = () => {
 // error state is handled here
 return <StyledTextAreaContainer error={error}>
           <textarea />
         </StyledTextAreaContainer>
}
```
You can see that since TextArea doesn't contain clear button we passing it to `getCommonInputStyles`

That's it. I hope you'll find it useful in your daily work with such great library as `styled-components`