There are some cases in which you'll want to build a UI component variant to promote coherent UX across a product or a team of engineers. Quite often, this happens by overriding the default behaviors of the UI components available in your respective UI library, so you don't have to reinvent the wheel.
This scenario is no different with Fluent UI React. We've designed the UI components to allow style overrides. The following examples will walk you through the various ways you can use CSS-in-JS to override the default styling of the UI Components and create custom variants for your application.
Variant Scenario: Launch Button
So, for this example, we're creating a "Launch Button" that has a specific set of styling that overrides the default Button provided by Fluent UI React. We want to keep all the built-in functionality of the Button
and override the following:
- Always change the shape to be circular
- Always have a Rocket Icon
- Always change the background color of the rest, hover, and pressed states
- Always is a large size
Here's an example of what we want the "Launch Button" to look like:
You'll notice that the overrides are always present—meaning our intent is that any consumer of this component isn't meant to be overridden themselves, thus promoting coherent UX across our theoretical product 😊.
In the next sections, we'll cover various ways we can override the Fluent UI React Button component and ways to structure it for sharing across a project.
Option 1: Create CSS style overrides and component props
The first option for creating a variant would be creating CSS-in-JS style overrides and configuring the props of the Button
component. Let's look at defining the CSS-in-JS style overrides using Griffle—the default CSS-in-JS engine in Fluent UI React:
const useLaunchButtonStyles = makeStyles({
launchButton: {
backgroundColor: "#ce000a",
...shorthands.borderColor("transparent"),
color: tokens.colorBrandBackgroundInverted,
":hover": {
backgroundColor: "#b10007",
...shorthands.borderColor("transparent"),
color: tokens.colorBrandBackgroundInverted
},
":hover:active": {
backgroundColor: "#5d0b00",
...shorthands.borderColor("transparent"),
color: tokens.colorNeutralForegroundOnBrand
}
}
});
We've created CSS-in-JS style definitions that overrides the backgound-color
, border-color
, and color
attributes of the Button
style. We also did this for the rest, hover, and pressed states o the button.
Next, we'll use the styles hook we created and apply the class to the Button
component via the className
prop:
const l = useLaunchButtonStyles();
<Button className={l.launchButton}>Launch</Button>
Now, this only overrides the color of the Button
component, which means we need to add the Rocket Icon and change the Button
shape and size:
const l = useLaunchButtonStyles();
<Button className={l.launchButton}
icon={<RocketRegular />}
shape="circular"
size="large">
Launch
</Button>
So, now we have a "Launch Button." The downside is that we need to tell any consumer that they need to configure the Button
component with a CSS class, an Icon, and a Shape. This means that potential consumers could either change the design or misconfigure the component to promote a consistent UX.
Let's see how we can solve this with our next option!
Option 2: Wrapper Component with style overrides and prop configuration
For this approach, we'll do exactly what we did in Option 1 above, but we'll wrap that up in a React Component.
Note: This technique does create a virtual node, so it's not recommended to have many layers of component wrapping as that can create performance issues. For cases of simple overrides not wrapped many times over, this approach meets our needs.
Once again, we'll take the same steps we did in Option 1, but wrap that up in a React Functional Component:
const useLaunchButtonStyles = makeStyles({
launchButton: {
backgroundColor: "#ce000a",
...shorthands.borderColor("transparent"),
color: tokens.colorBrandBackgroundInverted,
":hover": {
backgroundColor: "#b10007",
...shorthands.borderColor("transparent"),
color: tokens.colorBrandBackgroundInverted
},
":hover:active": {
backgroundColor: "#5d0b00",
...shorthands.borderColor("transparent"),
color: tokens.colorNeutralForegroundOnBrand
}
}
});
const LaunchButton: React.FC<LaunchButtonProps> = () => {
const l = useLaunchButtonStyles();
return (
<Button
icon={<RocketRegular />}
className={classes}
shape="circular"
size="large"
>
Launch
</Button>
);
};
export default LaunchButton;
Now, when a consumer imports the LaunchButton
they get all the consistency wrapped up into a functional component:
<LaunchButton />
This works great in guaranteeing coherent UX across a product, but we've essentially taken away any further downstream customization to consumers. If this was your intent—you're done. Yay! 🎉
But, let's pretend someone on your team says, "The Design team wants this feature to have a green "Launch Button" in this one specific feature. How can we do that?"
Well, you could create another wrapper component and call it LaunchButtonGreen
. However, as you imagine more and more color requests in the future, you know this isn't scalable. So, let's leverage the existing styling capabilities in the Fluent UI React components and bring that to your variant.
In the next option, we'll look at allowing CSS-in-JS style overrides to let consumers control the color of the LaunchButton
but keep everything else fixed.
Option 3: Wrapper component with overrides and allowing consumer style overrides
Now, we'll build on everything we've done in Option 2, but we'll allow consumers of the LaunchButton
to pass style overrides via the className
prop (just like we did with the Fluent UI React Button
).
The interesting requirement we need to think about here is the order of the style overrides being applied. We'll leverage the mergeClasses API in Griffel which merges and dedupes the atomic classes generated by makeStyles API.
And, because order is important for mergeClasses
we'll make sure consumer styles passed in via the className
prop always win over the default LaunchButton
styles:
const useLaunchButtonStyles = makeStyles({
launchButton: {
backgroundColor: "#ce000a",
...shorthands.borderColor("transparent"),
color: tokens.colorBrandBackgroundInverted,
":hover": {
backgroundColor: "#b10007",
...shorthands.borderColor("transparent"),
color: tokens.colorBrandBackgroundInverted
},
":hover:active": {
backgroundColor: "#5d0b00",
...shorthands.borderColor("transparent"),
color: tokens.colorNeutralForegroundOnBrand
}
}
});
export type LaunchButtonProps = Pick<
React.HTMLAttributes<HTMLElement>,
"className"
>;
const LaunchButton: React.FC<LaunchButtonProps> = ({ className }) => {
const l = useLaunchButtonStyles();
const classes = mergeClasses(l.launchButton, className);
return (
<Button
icon={<RocketRegular />}
className={classes}
shape="circular"
size="large"
>
Launch
</Button>
);
};
export default LaunchButton;
Here's an usage example from a consumer overriding the LaunchButton
styles:
const useLaunchButtonOverrides = makeStyles({
overrides: {
backgroundColor: "green"
}
});
const o = useLaunchButtonOverrides();
<LaunchButton className={o.overrides} />
Now, your variant has a similar developer experience as any other Fluent UI React component—a beautiful thing! It also gives the consumer a nice balance of customization and control over UX qualities that need to be coherent and consistent.
Want to know more? Checkout the CodesandBox for the full sample:
Let us know what kind of content you'd like to see from Fluent UI React by visiting us on:
GitHub: https://github.com/microsoft/fluentui
Docs: https://react.fluentui.dev
Twitter: https://twitter.com/fluentui
Thanks!
Top comments (0)