@fluentui/react-theming

    0.50.0 • Public • Published

    Summary

    react-compose provides a set of tools for creating themable components. The compose tool takes a functional component and, given default options and contextual overrides provided through React context, computes injected props including classes, slots, and slotProps. Styles are computed only when encountering unique theme objects per component, resulting in optimized performance.

    Example:

    import { SliderBase } from './Slider.base';
    import { compose } from '@fluentui/react-theming';
    
    export const Slider = compose(SliderBase, {
      // the themable name of the component
      name: 'Slider'
    
      // the set of replacement tokens to be injected into the styles
      tokens,
    
      // The css styling to be injected as a `classes` prop
      styles
    
      // A dictionary of React components representing the actual subcomponents to be used by the Slider
      slots
    });
    
    // Composed components can be recomposed to offer slightly different permutations.
    export const RedSlider = compose(Slider, {
      tokens: { railColor: 'red' }
    });
    

    Theming can be achieved through applications using a ThemeProvider component, also provided in the library:

    // Create a theme with a token override for the Slider.
    const theme = createTheme({
      components: {
        Slider: {
          tokens: {
            railColor: 'green',
          },
        },
      },
    });
     
    // Render the slider using the theme.
    const App = () => (
      <ThemeProvider theme={theme}>
        <Slider />
      </ThemeProvider>
    );

    Principles

    compose follows 3 principles:

    • Prefer faster components over more flexible components (if a decision between the 2 must be made.)

    • Decouple theming/styling from implementation in order to ensure the styling approach can be replaced without rewriting the component.

    • Keep it simple; avoid adding too many concepts for developers to learn.

    Who is compose for?

    Components created with compose are meant for use by anyone.

    compose itself is primarily intended for use by component authors; it provides a consistent means of attaching to a "themed context".

    compose can also be used by application developers to enable a greater level of customization when the "theme" does not provide enough flexibility.

    When should I use compose?

    • When creating a component that should be themeable.
    • When overriding major details of an existing component. (Explained later in "Overriding (tokens|slots) with a new component".)

    What's in a theme?

    A Fluent UI theme contains several major sections. At at high level, it contains detail about colors, types, effects, spacing, and animation. Furthermore, a theme has the ability to override major details about each and every component's look and feel as well as behavior.

    TODO: document theme

    Using components created with compose

    There is no requirement around using components build with compose. If you use them without a theme, they will render with whichever default token/styling values, if any, are provided.

    It is recommended the default styling provide very limited styling, leaving tokens as the preferred method for customizing the component.

    Styling components with a theme

    Default styling and tokens can always be overridden using theme.

    TODO: how would one replace styling completely, instead of overriding it?

    Customizing with tokens

    Frequently, individual products design needs conflict with that of the base style. To accomodate necessary changes, Tokens exist to allow easy modification of most all aspects of look and feel.

    A token is a key that corresponds to a value, usually from from the applied theme. Examples of tokens might be fontSize, fontFamily, borderRadius, animationDuration, and labelHoveredbBackground.

    Setting tokens with a theme

    To understand the set of tokens that a specific component understands, refer to the documentation of that component. For this example, we will assume that a Button component exists that supports the following tokens:

    • backgroundColor
    • fontSize
    • backgroundHoverColor

    To override any (or all) of the Button's tokens, an object should be provided within the theme under:

    {
      "components": {
        "Button": {
          "tokens": {
            "values": "here..."
          }
        }
      }
    }

    Tokens are represented by the following:

    1. Function

      A functional token is the preferred method of adjusting look and feel. Functional tokens reference values in the applied theme.

      {
        "components": {
          "Button": {
            "tokens": {
              "fontSize": t => t.fonts.base,
              "backgroundColor": t => t.colors.brand[2]
            }
          }
        }
      }
    2. Literal value

      A literal value allows a token to be hard-coded. It is considered the least desirable (as it will never be affected by other changes in the theme).

      A literal token in practice looks like:

      {
        "components": {
          "Button": {
            "tokens": {
              "fontSize": 12
            }
          }
        }
      }
    3. Dependent value

      There are several cases where the value of a token is based on a calculation of another value. For instance, the background hover color of a button might be desired to be a shade lighter than the default background color of the button. (In order to specify this, assume we have a lighten() function available.)

      {
        "components": {
          "Button": {
            "tokens": {
              "backgroundHoverColor": {
                dependsOn: ['backgroundColor'],
                value: ([backgroundColor: Color]) => lighten(backgroundColor)
              }
            }
          }
        }
      }

    Customizing tokens by creating variants

    If adjusting all instances of a component with a token override is not desirable, then it is possible to create a component variant using compose.

    For instance, it might be the case that most Button instances should look a certain way, but Button instances in a toolbar should not inherit the same tokens.

    The solution is to create a component that can apply different tokens, but retains the same underlying behavior.

    To create a new component that can be targeted separately from the base component, simply call compose and optionally provide new tokens.

    const ToolbarButton = compose(Button, {
      tokens: {
        fontSize: t => t.font.small
      }
    });

    Customizing with slots

    While tokens affect the look and feel of rendered elements, slots provides a way to make more significant adjustments to a component's structure and behavior.

    A slot is a rendered DOM element or higher level control that can be replaced at runtime.

    As an example, a Checkbox might choose to render a label element to hold descriptive text. If a use-case called for a proprietary <MyLabel /> control instead of a label, that slot could be targeted for replacement.

    Overriding slots with a theme

    To override a slot from a theme, specify a reference to the component in the theme.

    import { MyLabel } from 'my-library';
    {
      "components": {
        "Checkbox": {
          "slots": {
            "label": MyLabel
          }
        }
      }
    }

    Overriding slots with a new component

    compose can also specify slot assignments directly.

    import { MyLabel } from 'my-library';
     
    const MyCheckbox = compse(Checkbox, {
      slots: {
        label: MyLabel
      }
    });

    Creating a component meant for use with compose

    Components that work well with compose consist of 2 parts: an unstlyled based component and a composed layer that glues look and feel to the base component.

    This section first describes how tokens and styles are calculated, then explains what an unstyled base component must do in order to be a good citizen in the compose world.

    Understanding tokens

    Tokens are the exclusive means of getting data from a theme into a component. Tokens should be specified for every aspect of a control's look and feel.

    Tokens should be named according to the following anatomy:

    {slot (or none for root)}{property}{state (or none for default)}
    

    Examples:

    • thumbSizeHovered
    • backgroundColor
    • labelBorderDisabled

    TODO: Exhaustive description of token declarations

    Understanding styles

    After evaluating tokens, the tokens are passed to a style function. The style function should return an object which can be rendered by JSS.

    Example:

    const styles = (tokens: MyComponentTokens) => {
      return {
        root: {
          backgroundColor: tokens.backgroundColor,
          '&:hover': {
            backgroundColor: tokens.backgroundHoverColor
          }
        },
        widget: {
          borderColor: tokens.borderColor
        }
      };
    }

    Understanding slots

    Components should define a set of logical elements that are reasonable to replace. Additionally, sensible defaults should be provided. Slots provide an opportunity for callers to late-bind sections for replacement.

    TODO: examples of more slots

    Writing the base component

    Any functional component can be used with compose. However, there are several conventions that should be respected in order to make the user experience predictable.

    A good base component deviates from a run-of-the-mill component in 3 ways:

    • It should have no built-in opinion of styling. When styled via compose, class names will be passed in via slotProps to provide styling.
    • It accepts a prop named slots, which define the component to use for subcomponents.
    • It accepts a prop named slotProps, which will be handed off to subcomponents.

    States

    Each component should ennumerate the possible set of states as a set of boolean flags.

    For instance, a checkbox might declare these flags:

    • checked
    • readonly
    • disabled
    • labeled

    NB: States should be boolean values only.

    These states affect what classNames are selected to render on the root element of a component.

    For instance, in the case of a checkbox, the previous states would cause those selectors to appear on the root making them available to all downlevel slots.

    Slots

    TODO: Describe how to interact with slots

    Slot Props

    TODO: Describe how to interact with slotProps

    Building in practice

    A simple base component that renders a button might look like the following:

    interface Props {
      slots;
      slotProps;
      children;
      onClick;
    }
    const BaseButton: React.FunctionComponent<Props> = (props: Props) => {
     
      // First, define the slots
      // define `Root` as a const which renders the root.
      // Default to a button element.
      const { root: Root = 'button' } = props.slots || {};
     
      // Break out slot props to be passed to various components.
      // Mix in the props specified directly in props.
      const { root: rootProps } = props.slotProps || {};
     
      const resolvedRootProps = { ...rootProps, onClick: props.onClick };
     
      // Finally, render the component
      return <Root {...resolvedRootProps}>{props.children}</Root>
    }

    As components grow and become more complex, it is expected that hooks will be developed to resolve state and intelligently merge props into slotProps.

    Conformance

    TODO: Describe how to run conformance tests to make sure that base components appropriately react to theme changes.

    Keywords

    none

    Install

    npm i @fluentui/react-theming

    DownloadsWeekly Downloads

    30

    Version

    0.50.0

    License

    MIT

    Unpacked Size

    202 kB

    Total Files

    90

    Last publish

    Collaborators

    • microsoft1es
    • ecraig12345_msft
    • justslone
    • chrisdholt
    • miroslavstastny
    • levithomason
    • uifabricteam
    • uifrnbot
    • dzearing
    • layershifter
    • ling1726
    • allsnow
    • travisspomer