javascriptcssreactjsfluent-uifluentui-react

Why are the CSS variables in my FluentUI React app undefined?


I have a fairly complex React App (v18) using FluentUI (v8) and building with Vite (v5). There are a collection of CSS variables which are influenced by the theme, but for my app they seem to be largely undefined. I do have a ThemeProvider defined. I'll try to include the relevant code.

const App = () => {
  ...

  const theme = createTheme({
    palette: {
      themePrimary: '#ff462d',
      themeLighterAlt: '#fff8f7',
      themeLighter: '#ffe1de',
      themeLight: '#ffc8c0',
      themeTertiary: '#ff9082',
      themeSecondary: '#ff5c47',
      themeDarkAlt: '#e63f29',
      themeDark: '#c23523',
      themeDarker: '#8f271a',
      neutralLighterAlt: '#faf9f8',
      neutralLighter: '#f3f2f1',
      neutralLight: '#edebe9',
      neutralQuaternaryAlt: '#e1dfdd',
      neutralQuaternary: '#d0d0d0',
      neutralTertiaryAlt: '#c8c6c4',
      neutralTertiary: '#a19f9d',
      neutralSecondary: '#605e5c',
      neutralPrimaryAlt: '#3b3a39',
      neutralPrimary: '#323130',
      neutralDark: '#201f1e',
      black: '#000000',
      white: '#ffffff',
    },
    components: {
      IconButton: {
        styles: {
          rootDisabled: {
            opacity: 0.5
          },
          root: {
            opacity: 1,
            backgroundColor: '#fff'
          }
        }
      }
    }
  });

  ...

  return (
    <FluentProvider theme={webLightTheme}>
      <ThemeProvider theme={theme}>
        <RouterProvider router={router} />
      </ThemeProvider>
    </FluentProvider>
  );
};

And then I have a Callout with some Checkbox components inside of it:

    const filterDialog = (
        <Callout
            className={styles.filterDialog}
            role="dialog"
            target={`#${filterButtonId}`}
            onDismiss={hideFilters}
        >
            <Text as="h1" block variant="large">Filters</Text>
            <Stack horizontal horizontalAlign="space-between" tokens={{ childrenGap: '5px' }}>
                <Checkbox size="large" label={ filterCheckboxLabel("Feedback") } />
                <Checkbox disabled={!filterByFeedback} label={ filterCheckboxLabel("Liked?") } />
                <Checkbox disabled={!filterByFeedback} label={ filterCheckboxLabel("Disiked?") } />
            </Stack>
        </Callout>
    );
    
    return (
        <Stack className={styles.top}>
        ...
                        <IconButton id={filterButtonId} disabled={false} onClick={toggleFilters} iconProps={{ iconName: "filter" }} />
                        {showFilterDialog ? filterDialog : null}
                        <IconButton disabled={true} iconProps={{ iconName: "trash" }} />
                    </StackItem>
                </Stack>
            </StackItem>
            ...

The checkboxes didn't render correctly (the spacing was off and the check box border was too thick, so I looked at the dev console and saw this:

screenshot of missing CSS vars

I'm at a bit of a loss as to how to diagnose what the issue might be... I'm no expert on the inner workings of FluentUI, hopefully someone here is!

UPDATE: I can see that the FluentProvider is setting those variables... but for some reason the checkbox styles aren't able to access them?

enter image description here

UPDATE: I've made some progress here... it looks like surfaces such as Callout are rendered in a separate hierarchy from the one the FluentProvider is wrapping called the the "Default Layer Host" context. So the question has become "How do I wrap the default layer host context with the FluentProvider?"


Solution

  • The issue was caused by the Callout being rendered in a separate hierarchy. By creating a custom LayerHost component and pointing the callout at that using the layerProps.hostId property, I was able to render it under the ThemeProvider:

    ...
      return (
        <FluentProvider theme={teamsLightTheme}>
          <ThemeProvider theme={theme}>
            <RouterProvider router={router} />
            <LayerHost id="custom-layer-host" style={{ position: 'fixed', top: 0, left: 0, width: '100%' }}  />
          </ThemeProvider>
        </FluentProvider>
      );
    ...
    

    At first the callout was horizontally flattened, but adding those styles seems to have fixed the issue.

    Here's what the Callout looks like now:

    ...
        const filterDialog = (
            <Callout
                className={styles.filterDialog}
                role="dialog"
                target={`#${filterButtonId}`}
                onDismiss={hideFilters}
                layerProps={{ hostId: "custom-layer-host", }}
            >
                <Text as="h1" block variant="large">Filters</Text>
                <Stack horizontal horizontalAlign="space-between" tokens={{ childrenGap: '5px' }}>
                    <Checkbox checked={filterByFeedback} size="large" label={ filterCheckboxLabel("Feedback") } onChange={(_ev, data) => { data.checked ? setFilterByFeedback() : clearFilterByFeedback() }} />
                    <Checkbox checked={filterByFeedbackPos} disabled={!filterByFeedback} label={ filterCheckboxLabel("Liked?") } onChange={(_ev, data) => { data.checked ? setFilterByFeedbackPos() : clearFilterByFeedbackPos() }} />
                    <Checkbox checked={filterByFeedbackNeg} disabled={!filterByFeedback} label={ filterCheckboxLabel("Disliked?") } onChange={(_ev, data) => { data.checked ? setFilterByFeedbackNeg() : clearFilterByFeedbackNeg() }} />
                </Stack>
            </Callout>
        );
    ...