Theming & Styling with Spaceweb
Introduction
Styling in Spaceweb is done using CSS-in-JS, which from now own we'll refer to as JSS. It is a combination of two existing technologies TailwindCSS + Styletron.
Goals
Styling in Spaceweb was designed with the following goals in mind:
- Minimize size of (output) CSS
- Eliminate dead-code (CSS)
- Eliminate bleeding stylesheets (specificity hell)
- Theme-able
- SSR Support
- Encapsulation of the underlying technologies
The idea is to have a system that reduces the amount of style related code to be written, that is easy to maintain and alter on existing components.
Spaceweb Styling Tools
Spaceweb provides the following tools:
- Space container
- Space consumer
- Styled component generator
- Styling tools
Technical Terms
Before we take a look at all the styling tools available. These are some technical terms for reference.
/*** Theme-callback function (used to specify styles dynamically)*/type StyleFn<P = any> = (utils: StyleUtils,props: { $theme?: BaseTheme } & P) => StyleObject | string | undefined;/*** Shape of styles accepted by spaceweb.*/type StyleAtom = string | StyleObject | StyleFn | undefined;type Styles = StyleAtom | RecursiveArray<StyleAtom>;
All these types are defined in @sprinklrjs/spaceweb/types.
Defining Styles in Spaceweb
All the styling tools use the following nomenclatures to define styling:
- Utility-Classes
- JSS
- Theme callback
- Conditional Utility-Classes
- Combining All of Above
Utility-Classes
The first and preferred method for styling components is via utility classes. This approach is inspired and based in Tailwind's utility first approach.
Example:
Using utility classes in a string:
<div className={css('font-bold sm:w-full md:w-12/24')}>
The resulting style is equal to the following CSS:
/* classes are generated automatically by Styletron */.ae {font-weight: '700';}@media sreen and (max-width: 640px) {.af {width: 100%;}}@media sreen and (max-width: 640px) {.ag {width: 50%;}}
The className
attribute would be set to the following:
<div className="ae af ag"></div>
You can also use an array of strings to do the same, this is recommended to improve readability when dealing with numerous styles, or when you want to add styles programmatically on certain conditions:
const classes: string[] = ['font-bold', 'sm:w-full', 'md:w-12/24'];if (isDisabled) classes.push('hidden');return <div className={css(classes)}>{children}</div>;// orreturn <div className={css(...classes)}>{children}</div>;
Another way is to provide each utility-class as a separate string argument, again, recommended to increase readability when dealing with numerous utility-classes:
<div className={css('font-bold','sm:w-full','md:w-12/24',)}>
IMPORTANT:
Please note that the resulting styles are assigned to className
attribute, and not style
attribute. This is because based on the rules we generate CSS classes dynamically.
Done this way so that we can reduce the amount of CSS being generated, caching, to increase performance, and SSR support.
Using JSS
const { css, theme } = useStyle();return (<divclassName={css({color: theme.spr.text01,fontWeight: 700,width: calculatedValue,})}>{children}</div>);
IMPORTANT:
This way of styling components should be used ONLY when the intended style is not possible using the provided utility-classes.
We should always default to use utility-classes over JSS. Since doing so increases the cache-hits on previously generated CSS classes increasing performance overall.
Styling using Theme-Callback
If you need to access values from current theme, you can style your components using the Theme-Callback:
const { css } = useStyle();return (<divclassName={css(({ theme }) => ({backgroundColor: theme.spr.text01,}))}>{children}</div>);
Conditional Utility-Classes
Spaceweb provides classNames
utility method to add classes programmatically. The class will be added if the value is truthy.
import { classNames } from '@sprinklrjs/spaceweb/classNames';const MyComponent = () => {const { css } = useStyle();return <div className={css(classNames({ disabled: isDisabled }))}>{children}</div>;};
Combining All the Above
For complex styling scenarios, there are times when you need to combine multiple ways to express your style:
import { classNames } from '@sprinklrjs/spaceweb/classNames';<div className={css('font-bold','sm:w-full','md:w-12/24',classNames({ disabled: isDisabled }),{backgroundColor: rgb(2,0,36);},({ theme }) => ({fontFace: `Roboto, ${theme.font.sans}`}))} />
NOTE: The order of the arguments it doesn't matter, but we do recommend to use the following order as a convention:
- utility-classes
- conditional utility-classes
- JSS
- theme-callback
Styling Tools
Spaceweb provides the following styling tools:
- useStyle (context hook)
- css
- styled
useStyle
useStyle
is a context hook, which has the following signature:
const { css, getStyle, theme: { density }, direction } = useStyle();
NOTE: This is a hook, which means that it can only be used inside a React Component, which is rendered inside an application wrapped with <Space theme={light}>
container (which sets-up the theme in context).
css
This is a method that is returned from the useStyle
hook (see above section).
css
has the following nomenclature:
type CssFn = (...args: Styles[]) => string;
css
returns a string with all the generated CSS classes.
Example
import { classNames } from '@sprinklrjs/spaceweb/classNames';<div className={css('font-bold','sm:w-full','md:w-12/24',classNames({ disabled: isDisabled }),{backgroundColor: rgb(2,0,36);},({ theme }) => ({fontFace: `Roboto, ${theme.font.sans}`}))} />
getStyle
This is a method that is returned from the useStyle
hook (see above section).
getStyle
is specially useful for overriding styles on existing components. In which you need to generate the overriding JSS style rules.
This method has the same signature of css
, but instead of returning a string with the list of classes, it returns the generated JSS style.
type GetStyle = (...args: Styles[]) => StyleObject;
Example:
const getLabelOverride = () => {const { getStyle, theme } = useStyle();return getStyle({ backgroundColor: theme.spr.bgBlue });};
styled
styled
is a method that helps us to generate components with a pre-defined style for later use, which helps to reducing the amount of boilerplate code.
Components generated this way behave like any other component, all the properties that the generated component receives are forwarded to the styled component.
styled
has similar signature as the previous two, except that the first parameter is either a element name or a component that we wish to style:
type Tag = keyof JSX.IntrinsicElements | React.ComponentType<any>;type StyledComponent = (props: ComponentProps<unknown>) => ReactElement;type StyledFunc = (tag: Tag, ...args: Styles[]) => StyledComponent;
Example:
// Header.tsx;import styled from '@sprinklrjs/spaceweb/style/styled';export default styled('h1', 'font-hairline text-xl');
Then, the generated component can be used as follows:
<Header className="spr-text-01" />
NOTE: Classes passed to styled components will be merged and will take precedence over the ones specified originally.
Using Theme Values on Styled Components
If you need to access values from the theme to be selected, you can follow the following nomenclature:
// Header.tsx;import styled from '@sprinklrjs/spaceweb/style/styled';export default styled('h1', ({ theme }) => ({ color: theme.spr.text01 }));
Althought, the styled component doesn't have access to the theme at compile time, when used in other components the values for the theme will be present at run-time.
Using Component Props on Styled Components
Sometimes we need to use the properties from the component to make styling decisions. You can access the component properties via the theme callback as follows:
// Buttonimport styled from '@sprinklrjs/spaceweb/style/styled';export default styled('button', (utils, props) => [props.variant.primary ? 'spr-text-01' : 'spr-text-02',props.variant.primaryBg ? 'spr-ui-01' : 'spr-ui-02',]);
Space Consumer
In a typical React application, data is passed top-down (parent to child) via props, but this can be cumbersome for certain types of props (e.g. locale preference, UI theme) that are required by many components within an application. Context provides a way to share values like these between components without having to explicitly pass a prop through every level of the tree.
<SpaceConsumer>
subscribes to changes of Spaceweb context, which requires a function as a child. The function receives the current context that includes css
and getStyle
functions, and the current theme
.
Example:
render(): ReactElement {return (...<SpaceConsumer>{([{ css, getStyle }, { theme }]) => (<div className={css({ color: theme.spr.text01 })}>...</div>)}</SpaceConsumer>...);}