DEV Community

Cover image for Creating Custom Variants with Fluent UI React v9
Paul Gildea
Paul Gildea

Posted on

Creating Custom Variants with Fluent UI React v9

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:

  1. Always change the shape to be circular
  2. Always have a Rocket Icon
  3. Always change the background color of the rest, hover, and pressed states
  4. Always is a large size

Here's an example of what we want the "Launch Button" to look like:
Screenshot of the Launch Button

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
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Now, when a consumer imports the LaunchButton they get all the consistency wrapped up into a functional component:

<LaunchButton />
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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} />

Enter fullscreen mode Exit fullscreen mode

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)