Styling¶
When a consumer uses a component, providing style overrides for the component should not be a fragile guessing game. We should be able to easily tweak component styling and also create variants for subcomponents without a lot of complex effort that's bound to break later on.
A component consists of dom elements, or "areas". Each of the areas should be targetable for styling. The styling applied to each area may depend on the state of the component as well as the contextual theme settings. So, styling should be defined as a function of these inputs:
// Take in styling input, spit out styles for each area.
function getStyles(props: IComponentStyleProps): IComponentStyles {
return {
root: { /* styles */ },
child1: { /* styles */ },
child2: { /* styles */ }
};
}
With this in mind, let's make getStyles an optional prop to the component. Now style overrides can be applied in a functional way and even be conditionalized depending on the component state:
const getStyles = props => ({
root: [
{
background: props.theme.palette.themePrimary,
selectors: {
':hover': {
background: props.theme.palette.themeSecondary,
}
}
},
props.isExpanded
? { display: 'block' }
: { display: 'none' }
]
});
<MyComponent getStyles={ myStyleOverrides } />
Quite often, variants of a component which use the style must be created to abstract some customizations. A styled HOC is provided which can make creating variants simple:
import { styled } from 'office-ui-fabric-react/lib/Styling';
import { getStyles } from './MyComponentVariant.styles';
const MyComponentVariant = styled(
MyComponent,
getStyles
})
);
Within the component implementation, when we need to convert these styles into class names, we will use a helper called classNameFunction to create a memoized function which can translate the style objects into strings:
const getClassNames = classNameFunction<ICompStyleProps, ICompStyles>();
class Comp extends React.Component {
public render() {
const { getStyles, theme } = this.props;
const classNames = getClassNames(getStyles, { /* style props */ });
return (
<div className={ classNames.root }>...</div>
);
}
}
Component anatomy¶
A component should consist of these files:
ComponentName.props.ts- The interfaces for the component. We separate these out for documentation reasons.ComponentName.base.tsx- The unstyled component. This renders DOM structure and conrtains logic, MINUS styling opinions.ComponentName.styles.ts- Exports agetStylesfunction for the component which takes inIComponentNameStylePropsand returnsIComponentNameStyles.
Once these are defined, you can export the component which ties it all together:
ComponentName.tsx- Using thestyledhelper, exports a new component tying the base component to 1 or more style helpers.
Additionally, each component should have these:
ComponentName.checklist.ts- A checklist status export which allows the documentation to render notes on what validation has been done on the component.ComponentName.test.tsx- Unit tests for the component.
The idea is that components, especially reusable atomic components, should by default be exported unstyled. This gives us the flexibility to create variants.
ComponentName.props.ts changes¶
The props file should contain these 4 interfaces, in addition to any enums or consts externally required:
- IComponentName - The public method accessible through
componentRef. This should include thefocusmethod, as well as getters for important values likecheckedin the case the component will be referenced and the value may be read manually. Example:
export IComponentName {
focus: () => void;
}
- IComponentNameProps - The props for the component. This should include the
componentRefprop for accessing the public interface, thethemeprop (which will be injected by the@customizabledecorator), as well as thegetStylesfunction.
Example:
export IComponentNameProps extends React.Props<ComponentNameBase> {
componentRef?: (componentRef: IComponentName) => void;
theme?: ITheme;
getStyles?: IStyleFunction<IComponentNameStyleProps, IComponentNameStyles>;
}
- IComponentNameStyleProps - The props needed to construct styles. This represents the simplified set of immutable things which control the class names. Note that things which were optional may be set to be required here, to simplify the style definitions:
export interface IComponentNameStyleProps {
theme: ITheme;
disabled: boolean;
checked: boolean;
}
- IComponentNameStyles The styles which apply to each area of the component. Each area should be listed here required, as an
IStyle, withrootalways representing the root element of the component:
export interface IComponentNameStyles {
root: IStyle;
child1: IStyle;
// etc.
}
In the style interface, always refer to the root element using the name root, for predictability in styling.
ComponentName.base.tsx contents¶
- The component shoud be named
{ComponentName}Base. - It should be decorated with the
customizabledecorator using{ComponentName}Baseas the target name. - It should use the
classNameFunctionhelper to create a className generation function.
Example:
import { IComponentNameProps } from ='./ComponentName.props';
const getClassNames = classNameFunction<IComponentNameStyleProps, IComponentNameStyles>();
export class ComponentName extends React.Component<...> {
public render() {
const { getStyles, theme } = this.props;
const classNames = getClassNames(getStyles, { theme: theme! });
return (
<div className={ classNames.root }>Hello</div>;
);
}
}
ComponentName.styles.ts¶
The styles file should export the getStyles function which takes in IComponentNameStyleProps and returns IComponentNameStyles (the default styling for the component.)
Note that the root element for styles should al
export function getStyles(props: IComponentNameStyleProps): IComponentNameStyles {
return {
root: {},
child: {},
etc.
};
}
Component.tsx¶
Tying the component to the style is made easy using the styled HOC wrapper, whch as input takes the base component and an object of 1 or more style function props.
import { styled } from 'office-ui-fabric-react/lb/Styling';
import { CompoenentNameBase } from './ComponentName.base';
import { getStyles } from './ComponentName.styles';
// Create a Breadcrumb variant which uses these default styles.
export const ComponentName = styled(
ComponentNameBase,
{
getStyles
}
);
Support for customizable sub-component styling¶
The component may also intend to provide customized styling for nested components. For example, a Breadcrumb component may want to render Crumb components, which may take in their own styles.
The recommended approach is to avoid exposing the individual Crumb props at the Breadcrumb layer. Instead, expose a way to provide an alternative component for the Crumb using the as prop convention. This lets the caller create their own Crumb variant and use it within the Breadcrumb. It also opens up other scenarios such as providing additional default props on the sub-component, or replacing it completely.
In the Breadcrumb case, we would expose an optional crumbAs prop. This would allow the consumer to make a Breadcrumb variant which renders red Crumb components:
const RedCrumb = styled(Crumb, props => ({ root: background: 'red' }));
render() {
return <Breadcrumb crumbAs={ RedCrumb } ... />
Additionally this "breadcrumb with red crumbs" scenario could be abstracted as its own component, by passing in the 3rd optional param to styled, which lets the caller provide new default prop values:
const RedBreadcrumb = styled(Breadcrumb, undefined, { crumbAs: RedCrumb });
render() {
return <RedBreadcrumb ... />
}
ComponentName.test.tsx¶
The test file should include:
- A snapshot test locking the component DOM structure down for the important states.
- Tests which simulate clicking on things, changing the state of the compoent, and validating things still work.
Moving styles from scss to ts¶
Basic conversion just means copying styles from scss into ts, making prop names camelCased instead of kebab-cased, and stringifying everything except for pixel values.
In addition, all static classnames embedded within the tsx file inside of the css helper function calls can now move into the styles file.
Styles in scss:
.list {
white-space: nowrap;
padding: 0;
margin: 0;
display: flex;
align-items: stretch;
}
Converted to ts:
list: [
'ms-Breadcrumb-list',
{
whiteSpace: 'nowrap',
padding: 0,
margin: 0,
display: 'flex',
alignItems: 'stretch'
}
],
Some scss special cases:
Mixins and Includes¶
Sass mixins are simply an informal way of using functions. Translating them into actual javascript, where you can reuse and import/export them, is really easy.
If you find some fabric-core mixins are missing, consider adding them to the @uifabric/styling package if they are highly reusable. However keep in mind that the PLT1 bundle size WILL be affected, so do this sparingly only for very common things.
font-size-x Variables¶
Use typesafe enums instead of the sass variables:
import { FontSizes } from 'office-ui-fabric-react/lib/Styling';
fontSize: FontSizes.small
Focus Rectangles¶
The styling package has a helper to provide consistent focus rectangles.
Footnotes¶
Motivations for Moving Away from SCSS¶
SCSS a build time process of expanding a high level css-like language into raw css. Our pipeline to load the raw css goes through a javascript conversion process and gets loaded on the page via a javascript library called load-themed-styles. Effectively, we have a complex build process which takes rules, converts them into JavaScript, and loads them dynamically.
This process is complicated and adds a number of limitations.
We Can't Register Classes Dynamically¶
Scenarios like "make this area of the screen use a different theme" become really complicated if build time is the only time for evaluations.
Bundle size and css loading heft with scss¶
If a button has 20 different possible states, using scss you must load the css for all 20 of the states pre-emptively, so you end up loading way more rules than you will ever actually use. There is no "plt1 styles vs delay loaded styles". The best you can do is to partition your css to specific modules, and delay load the modules. But in this model, you will still preempt loading a lot of rules that aren't used.
Sass also encourages "mixins" as a way to have one definition of styles that can be used in multiple places. This completely fights against bundle size, since mixins simply stamp duplicates copies of the same rules whereever they're used, resulting in bloated (but highly compressable) style definitions. The compression helps but all of this could be avoided by using a different approach to defining our styling.
Constant battle with specificity¶
Perhaps the most difficult thing to resolve is css specificity. Countless hacks have been implemented to "slightly tweak" styling of a thing in a particular context. If your rule is equally specific as an existing rule, you have a race condition; last one to register wins, resulting in hacks that only work sometimes. And even if your rule is more specific than an existing rule, there are no gates that can catch an existing rule being changed to be more specific later, resulting in breaking the workarounds.
We want a system which allows users to pass in their overrides, which can create new permutations of classes which are only 1 level of specificity deep, providing a consistent safe way to override the defaults.