Created On Apr 23, 2025 With Tags React, Pattern
When building UI components, Its pretty common that we wanna trigger effects on component based on events on parent component, such as hovering. While applying style to components is straightforward, cross-component styling can become messy without proper control. The Blog explore an elegant solution using Compound Pattern to tackle the contextual styling challenge.
Applying a hover effect to a Button
is fairly simple, A pure CSS solution is more than enough.
.Button { background-color: transparent; &:hover { background-color: gray; }}
What if we want to trigger styling transition when hovering on a parent component? Let's say a Card
. The intuitive solution is to let the parent component apply extra style to the Button.
There are multiple ways to implement this, just mention a few of them:
style
or className
props to the Button
.@import
syntax to reference .Button
styles if using CSS modules.[`${Button}`]: {...}
if using any CSS-in-JS
solutions.I will use CSS module as example for my personal perference 🙂.
@import "component/Button.module.css";.Card { &:hover .Button { background-color: gray; }}
The solution have several drawbacks:
Button
and Card
components together.background-color: gray;
in both component.As developers, we hate duplicate things. Thus, I would be happy to extract this out to have it in a util file. However, naming it as a util is inappropriate, since it only serves Button
component.
Compound Pattern excels at solving these type of issue by breaking component into multiple pieces that communicate in the background to accomplish certain behavior. Unlike normal implementations leverage context API to share states, browser's CSS cascade do the heavy lifting for us 😛.
Again, fairly simple style sheet.
.Button { background-color: transparent; &:hover { background-color: gray; }}.HoverContext { &:hover .Button { background-color: gray; }}
And in the React component code, a simple approach is to export the context className.
import * as React from 'react'import buttonStyle from './Button.module.css';function Button({ lable, onClick, className }: { lable: string, onClick?(): void,}) { return <div onClick={onClick} className={buttonStyle.Button}> <p>{lable}</p> </div>}Button.HoverContext = buttonStyle.HoverContext;export default Button;
For better reusability and code structure, we can wrap the functionality in a Higher-Order Component.
import * as React from 'react'import clsx from 'clsx';import buttonStyle from './Button.module.css';function Button({ lable, onClick }: { lable: string, onClick?(): void,}) { return <div onClick={onClick} className={buttonStyle.Button}> <p>{lable}</p> </div>}function HoverContext({ children }: { children: React.ReactElement<{ className: string }> }) { const originalProps = children.props; return React.cloneElement( children, { ...originalProps, className: clsx(originalProps.className, buttonStyle.HoverContext) } );}Button.HoverContext = HoverContext;export default Button;
On the consumer side, user can choose to opt-in the HoverContext
when needed.
<Button label="Click me" />// If exporting the className<div className={Button.HoverContext}> <Button label="Hover parent to see effect" /></div>// Using the HOC approach<Button.HoverContext> <Card> <Button label="Interactive card button" /> <Card/></Button.HoverContext>
By doing so, we archieved:
We pushed the Compound Pattern to a bit further in this blog, which put forward an elegant solution for context-dependent styling in React. By providing explicit opt-in mechanisms for style behaviors, we maintain component independence while enabling rich interactions between components.
This pattern works particularly well for design systems where you need consistent, composable behaviors across many components without tight coupling.