stylex icon indicating copy to clipboard operation
stylex copied to clipboard

RFC: Shared Constants (Media Queries)

Open nmn opened this issue 6 months ago • 30 comments

Motivation

The immediate motivation is to enable defining and exporting Media Queries the same way that variables can be shared using defineVars. In the future, this feature could be expanded to support additional share-able values.

Inspiration

From StyleX itself, the idea is to expand defineVars API to able to share more kinds of values. Further, we’re adding helper functions such as stylex.types.length and stylex.types.color which will be used to mark values of variables with a particular type. This will not only add additional type-safety to the variable types, but also insert @property CSS rules which gives those variables types in CSS itself, which enables certain unique features, such as being able to animate variables.

From CSS, there is an old proposal for @custom-media that hasn’t been implemented in any browser.

stylex.types explainer

We are already working on utility functions to lock down the types of particular variable values. So, when using defineVars you can assign a type to a particular variable.

import * as stylex from '@stylexjs/stylex';

export const colors = stylex.defineVars({
  primary: stylex.types.color('black'),
  // ...
});

Here, the value ’black’ will be validated by the compiler and a compiler error will be thrown if a non-valid color is passed in. It also changes the behaviour of the variable generated. An @property CSS rule will be generated that marks colors.primary as a <color> in CSS itself:

@propert --colors-primary {
  syntax: '<color>';
  inherits: true;
  initial-value: black;
}

This makes the variable itself animateable in CSS using transition or animation.

The static types of the variable would be affected as well, and you’d be forced to use the same function to define values within createTheme.

const dracula = stylex.createTheme(colors, {
  // ERROR: Expected a type.Color, got a string.
  primary: 'purple',

  // OK!
  primary: stylex.types.color('purple'),
});

We can also consider adding utility functions like stylex.types.rgb(0, 0, 0) in the future. As all of these functions are compiled away, we can ensure that tree-shaking removes all these functions from the JS bundle.

Proposal

The core proposal is to add special type for atRules to stylex.types. I.e a new helper function, stylex.types.atRule. We can also add convenience functions for stylex.types.media and stylex.types.supports if it makes sense.

However, unlike CSS variables, the value of a custom at-rule needs to remain constant and cannot be overridden in a theme. This conflicts with the way defineVars and createTheme work as a pair today. And so the proposal also includes a new function called defineConsts. This new function will work exactly like defineVars except the variables created with it cannot be overridden with createTheme. Additionally, certain types, like atRule will only be accepted within defineConsts and not defineVars.

Example

// globalTokens.stylex.js
import * as stylex from '@stylexjs/stylex';

export const media = stylex.defineConsts({
  sm: stylex.types.atRule('@media (min-width: 640px) and (max-width: 767px)'),
  md: stylex.types.atRule('@media (min-width: 768px) and (max-width: 1023px'),
  lg: stylex.types.atRule('@media (min-width: 1024px) and (max-width: 1279px)'),
  xl: stylex.types.atRule('@media (min-width: 1280px) and (max-width: 1535px)'),
  xxl: stylex.types.atRule('@media (min-width: 1536px)'),
});

export const colors = stylex.defineVars({
  primary: stylex.types.color('black'),
  // ...
});

Using it would be the same as using variables. You import and use the value.

import * as stylex from '@stylexjs/stylex';
import {media, colors} from './globalTokens.stylex'

const styles = stylex.create({
  base: {
    width: {
      default: '100%',
      [media.sm]: 500,
      [media.md]: 800,
      [media.lg]: 960,
    },
    color: colors.primary,
  },
});

Implementation Details

The implementation would be almost identical to how variables already work. The defineConsts call would output a variable with a media query value to the generated styles, and while generating the CSS file by combining all the collected styles, the media queries variables would be inlined with the actual value.

This same process can be generalized to variables in general where any variable that is never overridden can be inlined directly and any unused variable can be removed.

Optional Extras

As mentioned earlier, we can add some additional utilities to make things easier, namely:

  1. stylex.types.media
  2. stylex.types.mediaWidth
  3. stylex.types.mediaHeight
  4. stylex.types.supports

Here’s what the example above could look like:

import * as stylex from '@stylexjs/stylex';

export const media = stylex.defineConsts({
  sm: stylex.types.mediaWidth(640, 768),
  md: stylex.types.mediaWidth(768, 1024),
  lg: stylex.types.mediaWidth(1024, 1280),
  xl: stylex.types.mediaWidth(1280, 1536),
  xxl: stylex.types.media('(min-width: 1536px)'),
});

The main benefit of these convenience functions is reducing boilerplate.

nmn avatar Dec 22 '23 00:12 nmn