Skip to main content

Component style sheets

Components and their tree are styled through style sheets created with the createComponentStyles() method. For the purpose of this documentation, let's say we're building a button component that renders many elements and components, we would have a style sheet that looks something like the following.

import { createComponentStyles } from '@aesthetic/<integration>';
const styleSheet = createComponentStyles(() => ({
button: {
appearance: 'none',
backgroundColor: 'transparent',
border: 0,
cursor: 'pointer',
display: 'inline-flex',
fontSize: 'inherit',
margin: 0,
padding: '6px 8px',
textAlign: 'center',
textDecoration: 'none',
userSelect: 'auto',
verticalAlign: 'middle',
},
button_selected: { /* ... */ },
button_disabled: { /* ... */ },
before: { /* ... */ },
after: { /* ... */ },
}));

In the example above, the keys of the object are known as selectors, with each selector being a combination of element and optional modifier (separated by an underscore). This is similar to the popular BEM syntax, without the "block", as our style sheet is the block (since styles are isolated). Style sheets support as many selectors as needed!

Styling elements#

Selectors#

There are 2 types of selectors, the first being basic selectors, which includes pseudo elements, pseudo classes, and HTML attributes that are deterministic and do not have permutations. They can be defined as nested style objects directly on the element's style object.

const styleSheet = createComponentStyles((css) => ({
button: {
backgroundColor: css.var('palette-brand-bg-base'),
// ...
':hover': {
backgroundColor: css.var('palette-brand-bg-hovered'),
},
'[disabled]': {
backgroundColor: css.var('palette-brand-bg-disabled'),
opacity: 0.75,
},
},
// ...
}));

The other type is advanced selectors, which includes combinators, as well as pseudos and attributes that do have permutations. Furthermore, multiple selectors can be defined at once using a comma separated list.

Advanced selectors must be nested within a @selectors object as they can not be properly typed with TypeScript.

const styleSheet = createComponentStyles(() => ({
element: {
// ...
'@selectors': {
// Combinators must start with >, ~, or +
'> li': {
listStyle: 'none',
},
// Attributes must start with [ and end with ]
'[href*="foo"]': {
color: 'red',
},
// Pseudos must start with : or ::
':not(:nth-child(9))': {
display: 'hidden',
},
// Multiple selectors can be separated with a comma
':disabled, [disabled]': {
opacity: 0.75,
},
},
},
}));

Media and feature queries#

Media and feature queries can be defined within a style object using @media and @supports respectively. Both types require an object that maps query expressions to nested style objects.

const styleSheet = createComponentStyles(() => ({
button: {
display: 'inline-block',
// ...
'@media': {
'(max-width: 600px)': {
width: '100%',
},
},
'@supports': {
'(display: inline-flex)': {
display: 'inline-flex',
},
},
},
// ...
}));

Both @media and @supports may be nested within itself and each other.

You can utilize the design system token's for consistent media query breakpoints.

const styleSheet = createComponentStyles((css) => ({
element: {
'@media': {
[css.tokens.breakpoint.lg.query]: {
width: '100%',
},
},
},
}));

Font faces#

Fonts are special as they need to be defined on the document instead of an element, which should be done with a theme style sheet. However, we provide some convenience through the fontFamily property, which can accept one or many font face objects.

Unlike normal CSS font faces, a font face object requires a srcPath property, with a list of file paths, instead of a src property.

const styleSheet = createComponentStyles(() => ({
element: {
// ...
fontFamily: {
fontFamily: 'Open Sans',
fontStyle: 'normal',
fontWeight: 'normal',
srcPaths: ['fonts/OpenSans.woff2', 'fonts/OpenSans.ttf'],
},
},
}));

Keyframes#

Animations have the same semantics as fonts and should be defined on a document using a theme style sheet, but also like fonts, we provide some convenience through the animationName property, which accepts a single keyframes object.

const styleSheet = createComponentStyles(() => ({
element: {
// ...
animationName: {
from: { transform: 'scaleX(0)' },
to: { transform: 'scaleX(1)' },
},
animationDuration: '3s',
animationTimingFunction: 'ease-in',
},
}));

Fallbacks#

A rarely used but necessary feature for progressive enhancement and supporting legacy browsers. Fallbacks allow you to define one or many different values for a single property through the @fallbacks object.

const styleSheet = createComponentStyles(() => ({
element: {
// ...
display: 'inline-flex',
'@fallbacks': {
display: ['inline', 'inline-block'],
},
},
}));

Variants#

Variants are a staple feature of many components -- especially commonly used ones like buttons, alerts, and labels -- and encompasses everything from sizing (small, large) to palettes (positive, negative, etc).

With that being said, the guiding principle behind variants is that only 1 may ever be active at a time. If you need to apply more than 1, then you should use the element-modifier syntax mentioned at the beginning of the chapter.

To utilize variants, we define a @variants object on a per element basis that maps each variant (type:enum) using nested objects. Variant names are critically important as they must match what's passed to cx().

const styleSheet = createComponentStyles((css) => ({
button: {
// ...
'@variants': {
'size:sm': { fontSize: css.var('text-sm-size') },
'size:df': { fontSize: css.var('text-df-size') },
'size:lg': { fontSize: css.var('text-lg-size') },
'palette:brand': { backgroundColor: css.var('palette-brand-bg-base') },
'palette:positive': { backgroundColor: css.var('palette-positive-bg-base') },
'palette:warning': { backgroundColor: css.var('palette-warning-bg-base') },
},
},
// ...
}));

Variant names must be formatted correctly! Each name combines a type to an enumerated value with a :. Both the type and enum support alphanumeric characters, while the enum also supports _ and -. The type must start with a letter.

Applying variants#

How a variant gets applied is highly dependent on the integration you are using, but it basically boils down to the following class name generation. Pass an object of variants and their enumerations as the 1st argument!

const className = cx({ size: 'sm', palette: 'brand' }, 'button');

Handling defaults#

When handling default styles for a variant, you must define it as a variant instead of defining it on the element directly. This is necessary as it avoids style collisions and specificity issues.

// Correct
const styleSheet = createComponentStyles((css) => ({
button: {
'@variants': {
'size:sm': { fontSize: 14 },
'size:df': { fontSize: 16 },
'size:lg': { fontSize: 18 },
},
},
}));
// Incorrect
const styleSheet = createComponentStyles((css) => ({
button: {
fontSize: 16,
'@variants': {
'size:sm': { fontSize: 14 },
'size:lg': { fontSize: 18 },
},
},
}));

Compound variants#

When you need to set variant styles based on a combination of other variants, you can combine them using a + operator. This synax should be familiar as it's based on CSS.

Using the example above, say we want to bold the text when the size is large, and the palette is brand, we would do the following:

const styleSheet = createComponentStyles((css) => ({
button: {
'@variants': {
'size:lg': { fontSize: css.var('text-lg-size') },
'palette:brand': { backgroundColor: css.var('palette-brand-bg-base') },
'size:lg + palette:brand': { fontWeight: 'bold' },
},
},
}));

You can combine as many variants as you'd like! Just be sure the variant names are properly combined with +.

Overriding styles#

While we support variants per element, we also support overrides on the style sheet. When defined at this level, any override deemed active will be deeply merged into a single style sheet in the order of: base < color scheme < contrast level < theme.

This feature will override any selector, element, element at-rule (even their variants), or nested style object from the base style sheet! This makes it very powerful and very robust.

By color scheme#

Use the addColorSchemeOverride() method for overrides depending on the "light" or "dark" color scheme of the currently active theme. This is perfect for making slight changes to a theme between the two modes.

const styleSheet = createComponentStyles(() => ({
element: {
display: 'block',
color: 'gray',
},
}))
.addColorSchemeOverride('light', () => ({
element: {
backgroundColor: 'white',
color: 'black',
},
}))
.addColorSchemeOverride('dark', () => ({
element: {
backgroundColor: 'black',
color: 'white',
},
}));

This is equivalent to the native prefers-color-scheme media query.

<link href="themes/default.css" rel="stylesheet" />
<link href="themes/day.css" rel="stylesheet" media="screen and (prefers-color-scheme: light)" />
<link href="themes/night.css" rel="stylesheet" media="screen and (prefers-color-scheme: dark)" />

By contrast level#

Use the addContrastOverride() method for overrides depending on the "low" or "high" contrast level of the currently active theme. This is perfect for providing accessible themes.

const styleSheet = createComponentStyles(() => ({
element: {
display: 'block',
color: 'orange',
},
}))
.addContrastOverride('low', () => ({
element: {
color: 'red',
},
}))
.addContrastOverride('high', () => ({
element: {
color: 'yellow',
},
}));

This is equivalent to the native prefers-contrast media query.

<link href="themes/default.css" rel="stylesheet" />
<link href="themes/default-low.css" rel="stylesheet" media="screen and (prefers-contrast: low)" />
<link href="themes/default-high.css" rel="stylesheet" media="screen and (prefers-contrast: high)" />

By theme#

And finally, use the addThemeOverride() method for overrides depending on the currently active theme itself. This provides granular styles on a theme-by-theme basis, perfect for style sheets that are provided by third-parties.

const styleSheet = createComponentStyles(() => ({
element: {
display: 'block',
color: 'gray',
},
}))
.addThemeOverride('night', () => ({
element: {
color: 'blue',
},
}))
.addThemeOverride('twilight', () => ({
element: {
color: 'purple',
},
}));

Theme names must match the names passed to registerTheme() or registerDefaultTheme().

Rendering CSS#

Rendering a style sheet into CSS and injecting into the document is typically handled by an integration and abstracted away from the consumer (see useStyles() in the React package). However, if you would like to render styles manually, you may do so with the renderComponentStyles() method.

This method requires the style sheet instance as the 1st argument, and returns an object of class names mapped to their selector.

import { renderComponentStyles } from '@aesthetic/<integration>';
import styleSheet from './some/styleSheet';
const result = renderComponentStyles(styleSheet);

References#

The structure of style objects is based on types provided by the @aesthetic/sss and @aesthetic/style packages.