Layout Composition Using Styled Components Without CSS-in-JS

Ismayil Khayredinov
ITNEXT
Published in
3 min readMay 22, 2023

--

Image Source: Brooklyn Institute for Social Research

Layout composition is an emerging pattern that allows us to build truly reusable components that can adapt to various use cases that diverge so often in an evolving product design. There is a new generation of libraries, such as Radix UI, that finally allow us to use the tools functionally and not worry about their impact on the presentational layer of our applications.

A common approach to composition is to extend the root level component with additional properties that represent nested child components. This pattern is easy on the eyes and quite friendly - you know exactly what the descendants of the specific root component are.

const ProductCard = ({ addToCart }) => (
<Card>
<Card.Heading>Webcam</Card.Heading>
<Card.Illustration>
<img src="webcam.jpg" alt="Webcam promo image" />
</Card.Illustration>
<Card.Body>Really cool webcam</Card.Body>
<Card.Footer>
<Card.Footer.Button onClick={addToCart}>Add to cart</Card.Footer.Button>
</Card.Footer>
</Card>
)

I am not a big fan of CSS-in-JS, but there is one pattern that emerged out of it that I find quite beneficial: clear separation of presentational concerns from other stateful and transactional aspects of the application. When working with CSS we tend to litter our code with presentational class names instead of embracing a level of abstraction inherent to styled components. In reality, nothing is stopping us from creating styled components that do not use CSS-in-JS, and instead rely solely on CSS classes (scoped or not):

const Card = styled('section', { className: 'card' })

We could take our styled components one step further by enumerating all of available descendants for each styled component. You will probably notice how well this aligns with the way we think about our CSS selectors (especially if we use BEM).

const Card = styled('section', { className: 'card' }, {
Heading: styled('h2', { className: 'card__heading' }),
Illustration: styled('div', { className: 'card__illustration' }),
Body: styled('div', { className: 'card__body' }),
Footer: styled('nav', { className: 'card__footer' }, {
Button: styled('button', {
type: 'button',
className: 'card__footer__button'
})
})
})

Let’s see how the styled factory for such component hierarchy could look like in React:

import React, { ComponentProps, ElementType, PropsWithChildren, JSX } from "react";

// this is very primitive
// there are better alternatives such as mergeProps in react-aria
const mergeProps = <T extends ElementType>(
...args: Array<Partial<ComponentProps<T>>>
) => {
return args.reduce((acc, cur) => {
return {
...acc,
...cur,
className: [acc.className, cur.className].filter(Boolean).join(" "),
};
}, {} as JSX.LibraryManagedAttributes<T, any>);
};

export const styled = <T extends ElementType, E>(
ComponentType: T,
defaultProps: Partial<ComponentProps<T>>,
elements?: E
): React.FC<ComponentProps<T>> & E => {
const component = (componentProps: PropsWithChildren<ComponentProps<T>>) => {
return <ComponentType {...mergeProps<T>(defaultProps, componentProps)} />;
};

return Object.assign(component, elements);
};

We could take this one step further and apply styling to any another component in our library (as long as it supports className). For example, we could use Radix UI to make our card collapsible.

import * as Collapsible from '@radix-ui/react-collapsible';
import { ComponentProps, ForwardedRef, forwardRef } from "react";

export const Card = styled(Collapsible.Root, { className: 'card' }, {
Heading: styled('h2', { className: 'card__heading' }),
CollapseTrigger: forwardRef(({children, ...props}: ComponentProps<typeof Collapsible.Trigger>, ref: ForwardedRef<HTMLButtonElement>) => {
const Component = styled(Collapsible.Trigger, { className: 'card__collapse-trigger'})
return <Component ref={ref} {...props}>{children}</Component>
}),
Content: forwardRef(({children, ...props}: ComponentProps<typeof Collapsible.Content>, ref: ForwardedRef<HTMLDivElement>) => {
const Component = styled(Collapsible.Content, { className: 'card__content'})
return <Component ref={ref} {...props}>{children}</Component>
}),
Illustration: styled('div', { className: 'card__illustration' }),
Body: styled('div', { className: 'card__body' }),
Footer: styled('nav', { className: 'card__footer' }, {
Button: styled('button', {
type: 'button',
className: 'card__footer__button'
})
})
})

That’s it for now. Hopefully, you can draw some inspiration from this approach and apply it to reduce the boilerplate around building and styling layout elements in your application.

--

--

Full-stack developer, passionate about front-end frameworks, design systems and UX.