Skip to main content

Component style sheets

Components and their tree are styled through style sheets created with the createComponentStyles() method, which styles multiple elements, and createElementStyles(), which styles a single element. Choose the pattern that is right for you!

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((css) => ({
button: {
appearance: 'none',
backgroundColor: 'transparent',
border: 0,
cursor: 'pointer',
display: 'inline-flex',
fontSize: 'inherit',
margin: 0,
padding: css.unit(1),
textAlign: 'center',
textDecoration: 'none',
userSelect: 'auto',
verticalAlign: 'middle',
},
button_selected: {
/* ... */
},
button_disabled: {
/* ... */
},
before: {
/* ... */
},
after: {
/* ... */
},
}));

In the examples 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!

Defining styles#

The foundation of all styles are properties, where each key-value pair maps to a CSS declaration, and abides the following:

  • Property names are camel cased versions of their CSS equivalent property.
  • Vendor prefixed properties are not supported. Use the vendorPrefixer setting to enable this automatically.
  • Unit values that default to the defaultUnit (typically px) setting can be written as literal numbers.
  • Values that require quotes in the CSS output must manually handle the quotes within the string.

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 queries 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 supports 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'],
},
// Multiple font faces
fontFamily: [
{
fontFamily: 'Open Sans',
fontStyle: 'normal',
fontWeight: 'normal',
srcPaths: ['fonts/OpenSans.woff2', 'fonts/OpenSans.ttf'],
},
{
fontFamily: 'Open Sans',
fontStyle: 'normal',
fontWeight: 800,
srcPaths: ['fonts/OpenSans-Bold.woff2', 'fonts/OpenSans-Bold.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',
// Multiple animations
animationName: [
{
from: { opacity: 0 },
to: { opacity: 1 },
},
{
'0%': { bottom: 0 },
'100%': { bottom: '100%' },
},
],
},
}));

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');

When using createThemeStyles, the only selector available is "element".

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 +.

Variables#

Defines element level CSS variables, by mapping variable names to their value. Names can be in camel case or variable kebab case (prefixed with --). Useful for overriding root and theme CSS variables on a per element basis.

const styleSheet = createComponentStyles((css) => ({
element: {
'@variables': {
customSize: '1.5rem',
'--custom-size': '1.5rem',
},
},
}));

Variable values are not transformed in any way, so they must be explicit. For example, unitless values are not supported for values that require a unit suffix.

Expanded properties#

CSS has a concept known as shorthand properties, where multiple properties and their values can be declared with a single property. For example, border-width, border-style, and border-color can be combined in border.

However, in CSS-in-JS, shorthand properties cause issues when defined alongside their longhand properties, so Aesthetic offers an expanded form for a handful of shorthand properties (cue irony) through the @aesthetic/addon-properties package. The current shorthand properties that support an expanded form are: animation, background, border, borderBottom, borderLeft, borderRight, borderTop, columnRule, flex, font, listStyle, margin, offset, outline, padding, textDecoration, and transition;

To utilize the expanded form, define an object where each property within maps to an equivalent longhand property. Using the border example above, the object would look like the following:

const styleSheet = createComponentStyles((css) => ({
element: {
// Shorthand
border: '1px dashed red',
// Longhand
borderWidth: 1,
borderStyle: 'dashed',
borderColor: 'red',
// Expanded
border: {
width: 1,
style: 'dashed',
color: 'red',
},
},
}));

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);