loading...

πŸ’… Styled-Components: Extending Children

bendman profile image Ben Duncan Updated on ・3 min read

Even when styled-components often export different variations of the component it can still be useful to control the styles from the parent. For example, a parent might want to adjust the position of the child.

import { Button } from '../components/button'

const Modal = () => (
  <aside>
    {/* How do we customize this button? */}
    <Button>Close</Button>
  </aside>
)

While you could export version with different positioning styles or that takes a position prop, it has more to do with the parent and visual context than the child itself and could be cumbersome to export a different version or create a property for every possible visual context.

As you can see, this gets out of hand very quickly:

// ../components/button

// When you just want a centered button
export const ButtonBlockCenter = styled(Button)`
  margin-left: auto;
  margin-right: auto;
`
// Don't forget about using it in a flex container!
export const ButtonFlexCenter = styled(Button)`
  align-self: center;
`
// What about other alignments?
// You could pass in a prop, but this gets ugly:
// <ButtonFlex align="flex-end" />
export const ButtonFlex = styled(Button)`
  ${props => props.align ? `align-self: ${props.align};` : ''}
`

Real designs often come with edge cases, and any of these methods quickly get out of hand. How can we extend a child component at the call site, from the parent?

Creating a Local Extension

One solution for simple cases is to create a local variation of the component by extending it again:

import { Button } from '../components/button'

const CloseButton = styled(Button)`
  align-self: center;
`

const Modal = () => (
  <aside>
    <Button>Close</Button>
  </aside>
)

Use Nesting Rules

If you already have a styled ancestor that provides enough context, you could use nesting and references to extend your button. This can be more succinct if there are many children that require specific styles.

import { Button } from '../components/button'

const Container = styled.aside`
  /* ... container styles */

  /* Maybe this will only apply to what we want... */
  > ${Button} {
    align-self: center;
  }
`

const Modal = () => (
  <Container>
    <Button>Close</Button>
  </Container>
)

Danger!

  • Rules need to be specific
  • Extensions of the child are also styled
  • Fragments can be hiding more children at the same depth

If the rule written isn't specific enough it can have unexpected results on descendants. For example, a page wrapper that applies a descendant selector (.parent .descendant) instead of a child selector (.parent > .child) would effect any further descendant instances of the child. Also, any variations of the child or fragments that render the child at the top level would also get styled, and may not be obvious in the context of the page.

Check out this codepen for some examples of problems that can occur:

Advanced Cases: Complex Children

What about passing styles to different elements of the child? This can be useful in complex child components, for example when they are used for layout of their own children, or have wrapping elements for more complex behavior.

One case of this pattern can be seen is in ReactNative, where ScrollView takes a contentContainerStyle and a normal style prop which are applied to different elements.

This can also be used on the web in custom components, like for styling a <label> inside of a <FormField> component, or adding a background color or inner alignment to a custom <PageSection> component.

While you can just apply styles directly, this avoids some of the benefits of styled-components, like CSS preprocessing and using props.

I'll be writing a future post that talks about these advanced use cases.

Posted on Mar 2 by:

bendman profile

Ben Duncan

@bendman

I'm an American programmer living in France. I'm often on a bike. πŸš΄β€β™‚οΈβ›·πŸ•πŸ‡«πŸ‡·πŸ‡ΊπŸ‡Έ

Discussion

markdown guide
 

Thanks for the tip! I've done a bunch of crazy things with this library, but not this. It might actually help simplify some of the weird things I've done like giving children classNames just to target from the parent.

 

Personally I don't mind the use of className for targeting children, as it solves a few of the problems mentioned here by specifying which element you're targeting. As long as you follow some clear naming convention like BEM to avoid conflicts it sounds like a fine idea to me, reminiscent of vanilla CSS.

I'm certainly open to hear other opinions though!