Search code examples
javascriptcssreactjsstyled-components

How to select parent components variant in styled components?


In styled-components we can add contextual styles using component selector pattern. But how do we select specific parents variants to contextually style the child component? For example, we have three components here, Child, NoteWrapper, and Note.

const Child = styled.div<{ editing?: boolean }>`
  ${props => props.editing && css`some styles`}
`

const NoteWrapper = styled.div<{ compact?: boolean }>`
  ${props => props.compact && css`some styles`}
`

const Note = styled.input`

/* How do I style this when Child is editing and NoteWrapper is compact */

`

const App = () => {
  return (
   <Child editing>
    <NoteWrapper compact>
     <Note/>
    </NoteWrapper>
   </Child>
  )
}

`

With plain CSS we could do something like this


.child.editing .note-wrapper.compact .note {
  /* Contextual styles here */
}

I know I can easily use the editing and compact variables and pass it to the Note component. However, it would be difficult to do so if components are highly nested.

My question is how do I style Note when Child is editing and NoteWrapper is compact in styled-components selector pattern?

I don't think we can do something like this in styled-components in some way? Can we?

const Note = styled.input`
 ${Child.editingClass} ${NoteWrapper.compactClass} & {
  /* The contextual styles here*/
 }
`

Solution

  • As far as I know, you can't access editing or compact, since they're props and aren't "propagated" in CSS. But you can achieve the same result using either classes or data attributes.

    Here's how I'd do it:

    const App = () => {
      return (
      <Child data-editing={true}>
        <NoteWrapper data-compact={true}>
          <Note/>
        </NoteWrapper>
      </Child>
      )
    }
    
    const Child = styled.div`
    `
    
    const NoteWrapper = styled.div`
    `
    
    const Note = styled.input`
      ${Child}[data-editing="true"] ${NoteWrapper}[data-compact="true"] & {
        /* 
          Only applied when Child is editing and NoteWrapper is compact
        */
        color: red;
      }
    
    `
    

    Essentially, styled-components needs to produce valid CSS at the end of the day. ${Child} will be replaced by the custom-generated classname, like .sc-abc123. And so the end result is .sc-abc123[data-editing="true"], which is totally-valid CSS.

    You could also use classes, which looks a bit tidier:

    const App = () => {
      return (
      <Child className="editing">
        <NoteWrapper className="compact">
          <Note/>
        </NoteWrapper>
      </Child>
      )
    }
    
    const Child = styled.div`
    `
    
    const NoteWrapper = styled.div`
    `
    
    const Note = styled.input`
      ${Child}.editing ${NoteWrapper}.compact & {
        /* 
          Only applied when Child is editing and NoteWrapper is compact
        */
        color: red;
      }
    
    `
    

    I prefer to use data-attributes because it's clear that they're used for logical purposes, and not because editing is a CSS class with a bunch of styles. But that's a subjective preference, they both work equally well in terms of objective functionality :)