Frontend developer focused on inclusive design

Next.js & Fluent UI: Theming

These notes are based on setup Next.js and FluentUI project.

Fluent UI themes

Fluent UI themes are sets of tokens that represent design values, which can be assigned to CSS properties to achieve a consistent look and feel. FluentUI React library comes with several pre-made themes.

However, these notes use only two (2) of the provided themes:

  • Web Light (webLightTheme)
  • Web Dark (webDarkTheme)

Custom themes can be created using factory functions or by overriding or extending existing tokens. Note, it's crucial to use tokens instead of CSS variables directly because CSS variables can be changed.

In the context of Fluent UI theming, factory functions are used to create custom themes based on existing themes, or with a custom brand color palette.

Create Theme Provider

The Theme Provider sets and provides the current theme for a web application. It utilizes the React Context API to pass data between components, allowing them to share and pass data at different levels.

Thus, create ThemeProvider.js file inside src/app :

'use client';

/**
 * React library imports.
 *
 * @property {function} createContext - A function to create a new context object.
 * @property {function} useContext - A function to access a context object.
 * @property {function} useState - A hook to manage state within a component.
 */
import {
    createContext,
    useContext,
    useState,
} from 'react';

/**
 * Creates a new theme context with default values.
 *
 * @returns {ThemeContext} A new theme context object.
 */
const ThemeContext = createContext({});

/**
 * Provides the theme context to child components.
 *
 * @param {Object} props - Component props.
 * @param {React.ReactNode} props.children - Child components to be rendered.
 * @returns {JSX.Element} The rendered component.
 */
export const ThemeProvider = ({children}) => {
    // Default theme name.
    const defaultTheme = "light";
    // State hook to manage the theme state within the component.
    const [theme, setTheme] = useState(defaultTheme);

    return (
        <ThemeContext.Provider value={{theme, setTheme}}>
            {children}
        </ThemeContext.Provider>
    );
}

/**
 * Returns the current theme context object.
 *
 * @returns {ThemeContext} The current theme context object.
 */
export const useThemeContext = () => useContext(ThemeContext);

The purpose of the Theme Provider is to enable changing the FluentUI theme for controls in a web application. For instance, a toggle control can switch between Light and Dark themes.

Update context providers

The context providers for the web application are located in providers.js.

This file consists of providers from the FluentUI library, like RendererProvider, FluentProvider, and SSRProvider. This is also the place where the Theme Provider functionality should be integrated.

In providers.js file:

  1. Import Theme Provider: ThemeProvider and useThemeContext from ThemeProvider.js.
  2. Import FluentUI themes: webLightTheme and webDarkTheme from @fluentui/react-components.

In addition to importing necessary FluentUI providers and themes, also import makeStyles and tokens to set custom styles for a web page. Using makeStyles allows to create CSS classes with styles:

/**
 * A custom style class for Fluent UI components.
 *
 * @property {string} root - The root CSS class.
 */
const useStyles = makeStyles({
  root: {
    backgroundColor: tokens.colorBrandBackground2
  }
});

The code snippet above creates a CSS class named root that has a specific background color. The color comes from FluentUI's color library through tokens.

FluentUI-based application's main container (FluentProvider) comes with a pre-defined background color. Use a custom CSS class to change default styles.

Final preview ofย providers.js file:

'use client';

// Import necessary dependencies from React library.
import {
  useEffect,
  useState,
} from 'react';

/**
 * Theme-related components and hooks.
 *
 * @property {function} ThemeProvider - A component to provide the app's theme context to child components.
 * @property {function} useThemeContext - A hook to access the app's theme context object.
 */
import {
  ThemeProvider,
  useThemeContext,
} from './ThemeProvider';

/**
 * FluentUI dependencies.
 *
 * @property {function} createDOMRenderer - A function to create a DOM renderer for Fluent UI.
 * @property {function} RendererProvider - A component to provide the renderer to Fluent UI components.
 * @property {function} FluentProvider - A component to provide a Fluent UI theme to child components.
 * @property {function} SSRProvider - A component to support server-side rendering.
 * @property {Object} webLightTheme - A Fluent UI light theme object.
 * @property {Object} webDarkTheme - A Fluent UI dark theme object.
 * @property {function} makeStyles - A function to create styles for Fluent UI components.
 * @property {Object} tokens - A collection of Fluent UI design tokens.
 */
import {
  createDOMRenderer,
  RendererProvider,
  FluentProvider,
  SSRProvider,
  webLightTheme,
  webDarkTheme,
  makeStyles,
  tokens,
} from '@fluentui/react-components';

// Create a DOM renderer for Fluent UI.
const renderer = createDOMRenderer();

/**
 * A custom style class for Fluent UI components.
 *
 * @property {Object} root - The root CSS class.
 */
const useStyles = makeStyles({
  root: {
    backgroundColor: tokens.colorBrandBackground2
  }
});

/**
 * Providers component.
 *
 * This component wraps other components with a set of providers
 * for Fluent UI, SSR, and a custom renderer.
 *
 * @param {Object} props - The properties for the Providers component.
 * @param {React.ReactNode} props.children - The child components to be wrapped by the Providers.
 * @returns {React.Element} The Providers component with child components.
 */
export function Providers({ children }) {
  // Declare a state variable named 'hasMounted' and a function named 'setHasMounted' to update it.
  const [hasMounted, setHasMounted] = useState(false);

  useEffect(() => {
    setHasMounted(true);
  }, []); // add empty array as second argument to run only once

  if (!hasMounted) {
    return null;
  }

  // If the component has mounted, return a set of providers.
  return (
    <ThemeProvider>
      <RendererProvider renderer={renderer || createDOMRenderer()}>
        <SSRProvider>
          <WrappedFluentProvider>
            {children}
          </WrappedFluentProvider>
        </SSRProvider>
      </RendererProvider>
    </ThemeProvider>
  );
}

/**
 * WrappedFluentProvider component.
 *
 * This component wraps the FluentProvider with the theme context provided
 * by the ThemeProvider. It is used to ensure that the theme value
 * is available and properly passed to the FluentProvider.
 *
 * @param {Object} props - The properties for the WrappedFluentProvider component.
 * @param {React.ReactNode} props.children - The child components to be wrapped by the FluentProvider.
 * @returns {React.Element} The WrappedFluentProvider component with the FluentProvider and child components.
 */
const WrappedFluentProvider = ({ children }) => {
  // Get styles for Fluent UI components using makeStyles function.
  const styles = useStyles();
  // Get the current theme from the app's theme context using useThemeContext hook.
  const {theme} = useThemeContext();
  // Set the app's theme to a corresponding Fluent UI theme.
  const currentTheme = theme === "light" ? webLightTheme : webDarkTheme;

  return (
    <FluentProvider
      theme={currentTheme}
      className={styles.root}
    >
      {children}
    </FluentProvider>
  );
};

Note, the file includes a new WrappedFluentProvider component that wraps the FluentProvider. The useThemeContext() hook is called inside the WrappedFluentProvider component, which is a child component of the ThemeProvider. It prevents the theme value from being undefined.

Create page layout

The page layout is located in page.js file. It is based on FluentUI components:

"use client";

/**
 * Import necessary dependencies from '@fluentui/react-components'.
 *
 * @property makeStyles - A hook to create style objects with theme-aware values.
 * @property mergeClasses - A utility function to merge CSS class names.
 * @property shorthands - A set of shorthand utility functions for commonly used CSS properties.
 * @property typographyStyles - A collection of typography styles provided by Fluent UI.
 * @property tokens - A set of predefined design tokens (variables) provided by Fluent UI.
 * @property Text - A Fluent UI component for displaying text.
 * @property Card - A Fluent UI component for displaying content in a card.
 * @property Switch - A Fluent UI component for creating toggle switches.
 * @property Avatar - A Fluent UI component for displaying user avatars.
 */
import {
  makeStyles,
  mergeClasses,
  shorthands,
  typographyStyles,
  tokens,
  Text,
  Card,
  Switch,
  Avatar,
} from "@fluentui/react-components";

/**
 * Import necessary icon components from '@fluentui/react-icons'.
 *
 * @property PeopleStar16Filled - A Fluent UI icon component representing a group of people with a star.
 * @property CheckmarkStarburst16Filled - A Fluent UI icon component representing a checkmark in a starburst shape.
 * @property NotepadEdit16Filled - A Fluent UI icon component representing a notepad with a pencil for editing.
 */
import {
  PeopleStar16Filled,
  CheckmarkStarburst16Filled,
  NotepadEdit16Filled,
} from "@fluentui/react-icons";

/**
 * Import the useThemeContext custom hook from the ThemeProvider module.
 *
 * @property useThemeContext - A custom hook for accessing the current theme context and related functions.
 */
import { useThemeContext } from "./ThemeProvider";

// Create a custom 'useStyles' hook to define the styling for the Home component.
const useStyles = makeStyles({
  container: {
    ...shorthands.padding(tokens.spacingHorizontalXXL),
    ...shorthands.gap(tokens.spacingVerticalM),
    display: "flex",
    flexDirection: "column",
    alignItems: "center",
    justifyContent: "center",
    minHeight: "100vh",
  },
  settings: {
    display: "flex",
    flexDirection: "row",
    alignItems: "center",
    justifyContent: "space-between",
    width: "280px",
  },
  toggleButton: {
    marginInlineStart: `calc(-1 * ${tokens.spacingHorizontalS})`,
  },
  toggleIcon: {
    fontSize: "24px",
  },
  card: {
    width: "280px",
    height: "fit-content",
  },
  title: typographyStyles.subtitle2,
  flex: {
    ...shorthands.gap("4px"),
    display: "flex",
    flexDirection: "row",
    alignItems: "center",
  },
  labels: {
    ...shorthands.gap("6px"),
  },
  row: {
    display: "flex",
    alignItems: "flex-start",
    ...shorthands.gap("12px"),
  },
  column: {
    display: "flex",
    flexDirection: "column",
    alignItems: "flex-start",
    ...shorthands.gap("4px"),
  },
  label: {
    ...typographyStyles.body1Stronger,
    color: tokens.colorBrandForeground2,
  },
  caption: {
    ...typographyStyles.caption1,
    color: tokens.colorNeutralForeground3,
  },
  svg: {
    width: "12px",
    height: "12px",
  },
  footer: {
    ...shorthands.gap("12px"),
    paddingInlineStart: "42px",
    justifyContent: "space-between",
  },
});

/**
 * Home component.
 *
 * This component renders a simple 'Hello World!' message and
 * some text styled with Fluent UI components and utilities.
 *
 * @returns {React.Element} The Home component with the 'Hello World!' message and some text.
 */
export default function Home() {
  // Retrieve the styles object from the 'useStyles' hook.
  const styles = useStyles();
  const { theme, setTheme } = useThemeContext();

  // Act on toggle click.
  const handleToggleChange = (event) => {
    const selectedTheme = !event.currentTarget.checked ? "dark" : "light";
    setTheme(selectedTheme);
  };

  // Check if current theme is set to `light`.
  const isLightTheme = theme === "light" ? true : false;
  // Set label for control element based on selected theme.
  const toggleLabel = isLightTheme ? "Light Theme" : "Dark Theme";
  // Set icon for cotrol element based on selected theme.
  const toggleIcon = isLightTheme ? "๐ŸŒž" : "๐ŸŒš";
  // Data with information about actors.
  const actorsData = [
    {
      name: "Ashton Kutcher",
      description:
        "Christopher Ashton Kutcher is an American actor, producer, entrepreneur, and former model.",
    },
    {
      name: "Rebel Wilson",
      description:
        "Rebel Melanie Elizabeth Wilson is an Australian actress, comedian, writer, singer, and producer.",
    },
    {
      name: "Morgan Freeman",
      description:
        "Morgan Freeman is an American actor, director, and narrator.",
    },
  ];

  // Render the Home component with a Title1 and Text component from Fluent UI.
  return (
    <main className={styles.container}>
      <div className={styles.settings}>
        <Switch
          label={toggleLabel}
          checked={theme === "light" ? true : false}
          onChange={handleToggleChange}
          className={styles.toggleButton}
        />
        <span className={styles.toggleIcon}>{toggleIcon}</span>
      </div>
      <Card className={styles.card}>
        <header className={mergeClasses(styles.flex, styles.labels)}>
          <Text as="h1" className={styles.title}>
            Favorite Actors
          </Text>
        </header>

        {actorsData.map((actor, index) => (
          <div key={index}>
            <div className={styles.row}>
              <Avatar
                name={actor.name}
                badge={{
                  icon: <CheckmarkStarburst16Filled className={styles.svg} />,
                }}
              />
              <div className={styles.column}>
                <Text className={styles.label}>{actor.name}</Text>
                <Text className={styles.caption}>{actor.description}</Text>
              </div>
            </div>
          </div>
        ))}

        <footer className={mergeClasses(styles.flex, styles.footer)}>
          <div className={styles.flex}>
            <PeopleStar16Filled />
            <Text>3</Text>
          </div>

          <div className={styles.flex}>
            <NotepadEdit16Filled />
            <Text>Notes</Text>
          </div>
        </footer>
      </Card>
    </main>
  );
}

For demo purposes, the layout displays a small card with a title and a list of actors. A toggle control above the card lets users change the web application's theme.