Skip to content

Latest commit

Β 

History

History
465 lines (361 loc) Β· 15.4 KB

writing-styles-with-emotion.md

File metadata and controls

465 lines (361 loc) Β· 15.4 KB

Writing styles with Emotion

EUI uses Emotion when writing CSS-in-JS styles. A general knowledge of writing CSS is enough in most cases, but there are some JavaScript-related differences that can result in unintended output. Similarly, there are feaures that don't exist in CSS of which we like to take advantage.

File patterns

/* {component name}.styles.ts */
import { css } from '@emotion/react';
import { UseEuiTheme } from '../../services';

export const euiComponentNameStyles = ({ euiTheme }: UseEuiTheme) => {
  return {
    euiComponentName: css` // Always start the object with the first key being the name of the component
      color: ${euiTheme.colors.primaryText};
    `,
  };
};

πŸŽ‰ ProTip: VS Code snippet To make generating component boilerplate just a little bit easier, you can add the following block to a global or local snippet file in VS Code. Once saved, you'll be able to generate the boilerplate by typing `euisc` `tab`. Learn how to add snippets in VS Code:
"euiStyledComponent": {
  "prefix": "euisc",
  "body": [
    "/*",
    "* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one",
    "* or more contributor license agreements. Licensed under the Elastic License",
    "* 2.0 and the Server Side Public License, v 1; you may not use this file except",
    "* in compliance with, at your election, the Elastic License 2.0 or the Server",
    "* Side Public License, v 1.",
    "*/",
    "",
    "import { css } from '@emotion/react';",
    "import {",
    "  euiFontSize,",
    "  logicalCSS,",
    "} from '../../global_styling';",
    "import { UseEuiTheme } from '../../services';",
    "",
    "export const ${1:componentName}Styles = ({ euiTheme }: UseEuiTheme) => {",
    "  return {",
    "    ${1:componentName}: css`",
    "      ${2:property}: tomato;",
    "    `",
    "  };",
    "};"
  ],
  "description": "EUI styled component"
}

/* {component name}.tsx */
import { useEuiTheme } from '../../services';
import { euiComponentNameStyles } from './{component name}.styles.ts';

export const EuiComponent = () => {
  const theme = useEuiTheme();
  const styles = euiComponentNameStyles(theme);
  const cssStyles = [styles.euiComponentName]

  return (
    <div css={cssStyles} />
  );
};

CSS-aligned props

If a prop/value pair maps 1:1 to the CSS property: value, pass the value straight through. We encounter this scenario when it is apparent that a given css property is core to configuring a component, and it doesn't make sense to use an abstraction.

position?: CSSProperties['position'];

const cssStyles = [
  { position }
];

Component props that enable styles

Building an array of styles

Use an array inside of the css prop for optimal style composition and class name generation. This is relevant even if only a single style object is passed.

examples from avatar.tsx

export const EuiAvatar: FunctionComponent<EuiAvatarProps> = ({...}) => {
  // access the theme and compute avatar's styles
  const euiTheme = useEuiTheme();
  const styles = euiAvatarStyles(euiTheme);

  ...

  // build the styles array
  const cssStyles = [
    styles.euiAvatar, // base styles
    styles[size], // styles associated with the `size` prop's value
    styles[type], // styles associated with the `type` prop's value

    // optional styles
    isPlain && styles.plain,
    isSubdued && styles.subdued,
    isDisabled && styles.isDisabled,
  ];

  ...

  // pass the styles array to the `css` prop of the target element
  return (
    <div css={cssStyles} />
  )
}

If a prop's value renders no styles

A. If it's necessary to still know the prop value while debugging, create an empty css`` map for that value

paddingSize = 'none';

const euiComponentStyles = ({
  none: css``
})

B. If it's mostly just an empty default state, check for that prop before grabbing the css value

paddingSize = 'none';

const cssStyles = [
  paddingSize === 'none' ? undefined : styles[paddingSize]
]

Style maps

When building styles based on an array of possible prop values, you'll want to establish the array of values first in the component file then use that array to create your prop values and your styles map.

export const SIZES = ['s', 'm', 'l', 'xl', 'xxl'] as const;
export type EuiComponentNameSize = typeof SIZES[number];

export type EuiComponentNameProps = CommonProps & {
  size?: EuiComponentNameSize;
};

export const EuiComponentName: FunctionComponent<EuiComponentNameProps> = ({...}) => {
  const euiTheme = useEuiTheme();

  const styles = euiComponentNameStyles(euiTheme);
  const cssStyles = [styles.euiComponentName, styles[size]];

  return (
    <div css={cssStyles} />
  )
}
const componentSizes: {
  [size in EuiComponentNameSize]: _EuiThemeSize;
} = {
  s: 'm',
  m: 'base',
  l: 'l',
  xl: 'xl',
  xxl: 'xxl',
};

export const euiComponentNameStyles = ({ euiTheme }: UseEuiTheme) => ({
  euiComponentName: css``,

  // Sizes
  s: css`
    width: ${euiTheme.size[componentSizes.s]};
    height: ${euiTheme.size[componentSizes.s]};
  `,
  m: css`
    width: ${euiTheme.size[componentSizes.m]};
    height: ${euiTheme.size[componentSizes.m]};
  `,
  ...etc
});

Style helpers

EUI components often have style variants that use a similar patterns. In these cases, consider creating a helper function to create repetitive styles.

const _componentSize = ({
  size,
  fontSize,
}: {
  size: string;
  fontSize: string;
}) => {
  return `
    width: ${size};
    height: ${size};
    line-height: ${size};
    font-size: ${fontSize};
  `;
};

The helper function can then be used in the exported style block:

export const euiComponentNameStyles = ({ euiTheme }: UseEuiTheme) => ({
  // Sizes
  s: css(
    _componentSize({
      size: euiTheme.size.l,
      fontSize: euiTheme.size.m,
    })
  ),
  m: css(
    _componentSize({
      size: euiTheme.size.xl,
      fontSize: `calc(${euiTheme.size.base} * 0.9)`,
    })
  ),
  l: css(
    _componentSize({
      size: euiTheme.size.xxl,
      fontSize: `calc(${euiTheme.size.l} * 0.8)`,
    })
  ),
});

Note that the helper function returns a string literal instead of a css method from @emotion/react. This reduces the serialization work at runtime and makes the helper more flexible (e.g., could be used with a style attribute). Also note that the css method from @emotion/react can be called as a normal function instead of as a template literal.

Conditional styles

Styles can be added conditionally based on environment variables, such as the active theme, using nested string template literals.

`
    color: colors.primary;
    background: ${colorMode === 'light' ? 'white' : 'black'`}
`

Although possible in some contexts, it is not recommended to "shortcut" logic using the && operator. Use ternary statements to avoid undefined statments from entering the compiled code.

`${font.body.letterSpacing ? `letter-spacing: ${font.body.letterSpacing}` : ''`}`

Child selectors

Most components also contain child elements that have their own styles. If you have just a few child elements, consider having them in the same function.

export const euiComponentNameStyles = ({ euiTheme }: UseEuiTheme) => ({
  euiComponentName: css``,
  euiComponentName__child: css``
});
export const EuiComponentName: FunctionComponent<EuiComponentNameProps> = ({...}) => {
  const euiTheme = useEuiTheme();

  const styles = euiComponentNameStyles(euiTheme);
  const cssStyles = [styles.euiComponentName];
  const cssChildStyles = [styles.euiComponentName__child];

  return (
    <div css={cssStyles}>
      <span css={cssChildStyles} />
    </div>
  )
}

If you have multiple child elements, consider grouping them in different theme functions to keep things tidy. Keep them within a single styles.ts file if they exist in the same .tsx file.

export const euiComponentNameStyles = ({ euiTheme }: UseEuiTheme) => ({
  euiComponentName: css``
});

export const euiComponentNameHeaderStyles = ({ euiTheme }: UseEuiTheme) => ({
  euiComponentName__header: css``,
  euiComponentName__headerIcon: css``,
  euiComponentName__headerButton: css``
});

export const euiComponentNameFooterStyles = ({ euiTheme }: UseEuiTheme) => ({
  euiComponentName__footer: css``
});
export const EuiComponentName: FunctionComponent<EuiComponentNameProps> = ({...}) => {
  const euiTheme = useEuiTheme();

  const styles = euiComponentNameStyles(euiTheme);
  const cssStyles = [styles.euiComponentName];

  const headerStyles = euiComponentNameHeaderStyles(euiTheme);
  const cssHeaderStyles = [headerStyles.euiComponentName__header];
  const cssHeaderIconStyles = [headerStyles.euiComponentName__headerIcon];
  const cssHeaderButtonStyles = [headerStyles.euiComponentName__headerButton];

  const footerStyles = euiComponentNameFooterStyles(euiTheme);
  const cssFooterStyles = [footerStyles.euiComponentName__footer];

  return (
    <div css={cssStyles}>
      <div css={cssHeaderStyles}>
        <span css={cssHeaderIconStyles} />
        <button css={cssHeaderButtonStyles}>My button</button>
      </div>
      <div css={cssFooterStyles} />
    </div>
  )
}

Nested selectors

For the most part, nested selectors should not be necessary. If a child element requires styling based on the parent's variant, pass the same variant type to the child element.

export const euiComponentNameStyles = ({ euiTheme }: UseEuiTheme) => ({
  euiComponentName: css``,
  // Sizes
  s: css``,
  m: css``,
});

export const euiComponentNameChildStyles = ({ euiTheme }: UseEuiTheme) => ({
  euiComponentName__child: css``,
  // Sizes
  s: css``,
  m: css``,
});
export const EuiComponentName: FunctionComponent<EuiComponentNameProps> = ({...}) => {
  const euiTheme = useEuiTheme();

  const styles = euiComponentNameStyles(euiTheme);
  const cssStyles = [styles.euiComponentName, styles[size]];

  const childStyles = euiComponentNameChildStyles(euiTheme);
  const cssChildStyles = [childStyles.euiComponentName__child, childStyles[size]];

  return (
    <div css={cssStyles}>
      <span css={cssChildStyles} />
    </div>
  )
}

If for other reasons, it is absolutely necessary to target a child from within another selector, you should use the class attribute selector to match a part of the class string you expect to find.

export const euiComponentNameStyles = ({ euiTheme }: UseEuiTheme) => ({
  euiComponentName: css`
    [class*="euiComponentName__child"] {}
  `,
});

Creating colorMode specific components

When creating components that rely on a specific colorMode from <EuiThemeProvider>, use this pattern to create a wrapper that will pass the entire component <EuiThemeProvider> details.

  • _EuiComponentName is an internal component that contains the desired functionality and styles.
  • EuiComponentName is the exportable component that wraps _EuiComponentName inside of <EuiThemeProvider>.
const _EuiComponentName = ({ componentProps }) => {
  return <div />;
}

export const EuiComponentName = ({ componentProps }) => {
    const Component = _EuiComponentName;
    return (
      <EuiThemeProvider colorMode={ colorMode }>
        <Component {...componentProps} />
      </EuiThemeProvider>
    );
  }
);

Refer to EuiBottomBar to see an example of this pattern in practice and as an example of using forwardRef.

Emotion mixins & utilities

When creating mixins & utilities for reuse within Emotion CSS, consider the following best practices:

  • Publicly-exported mixins & utilities should go in src/global_styling/mixins. Utilities that are internal to EUI only should live in src/global_styling/functions.
  • If the mixin is simple and does not reference euiTheme, you do not need to create a hook version of it.
  • In general, prefer returning CSS strings in your mixin.
    • However, you should consider creating a 2nd util that returns a style object instead of a CSS string if the following scenarios apply to your mixin usage:
      • If you anticipate your mixin being used in the style prop instead of css (since React will want an object and camelCased CSS properties)
      • If you want your mixin to be partially composable, so if you think developers will want to obtain a single line/property from your mixin instead of the entire thing (e.g. euiFontSize.lineHeight)

Naming

When naming your mixins & utilities, consider the following statements:

  • Always prefix publicly-exported functions with eui unless it's purely a generic helper utility with no specific EUI consideration
  • When creating both a returned string version and object version, append the function name with CSS for strings and Style for objects. Example: euiMixinCSS() vs euiMixinStyle().

API pattern

For consistency, use the following pattern for style mixins that accept required and/or optional arguments:

const euiMixin = (
  euiTheme: UseEuiTheme;
  required: RequiredProperty;
  optional?: {
    optionalKey1?: OptionalProperty;
    optionalKey2?: OptionalProperty;
  }
) => {}

If the mixin does not accept required or optional properties, the argument can be removed.

Writing unit tests for output styles

If using complex utilities or calculations that leaves you unsure as to the output of your styles, it may be worth writing Jest snapshot tests to capture the final output. See EuiText's style snapshots or EuiTitle for an example of this.

If writing straightforward or static CSS, unit tests should be unnecessary.

FAQ

Can the css prop be forwarded to a nested element?

Emotion converts the css prop to a computed className value, merging it into any existing className prop on an element. We do not parse or handle these in any special way, so whichever element the className prop is applied to receives the styles created by Emotion. See https://codesandbox.io/s/emotion-css-and-classname-ohmqe7 for a playground demonstration.

Sometimes apps want or need to provide styles (or other props) to multiple elements in a component, and in these cases we add a prop to the component that captures the extra information, spreading it onto the element. We can continue with this approach, allowing the css prop to be added for flexible styling.

Which element in a custom component gets the css styling?

Same as the above answer, whichever element is given the generated className is the styles' target.

How should createElement usages be converted?

Emotion provides its own createElement function; existing uses of import {createElement} from 'react' can be converted to import {createElement} from '@emotion/react'