Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Typed CSS variable API #1684

Open
itsdouges opened this issue Jun 26, 2024 · 1 comment
Open

RFC: Typed CSS variable API #1684

itsdouges opened this issue Jun 26, 2024 · 1 comment
Labels
new feature 🆕 New feature or request

Comments

@itsdouges
Copy link
Collaborator

Is your feature request related to a problem? Please describe.

Currently when defining CSS variables through Compiled there's a lot left to be desired.

  • CSS variables aren't type safe
  • Declaration and usage aren't "connected" end-to-end, you have limited visibility during refactors for who is using your variables outside of literal searches and/or exposing your own variables
  • Interop of CSS custom properties and the strict typed APIs is problematic as defining CSS vars with template literal types like --${string} results in the "Type instantiation is excessively deep and possibly infinite" error
  • CSS variables aren't ensured to be globally unique

The current minimal declaration of a CSS variable in Compiled today would look like:

import { css } from '@compiled/react';

export const myVar = '--my-var';

const styles = css({
  [myVar]: 'red',
  color: `var(${myVar})`.
});

This works. But it's not good. It gets worse when wanting to try and use xcss prop or the strict APIs with variables. Currently CSS custom properties don't work with the strict API:

import { cssMap } from '@atlaskit/css';

const buttonStyles = cssMap({
	container: {
		'--hello-world': 'red',
                ^^^^^^^^^^^ Object literal may only specify known properties, and ''--hello-world'' does not exist in type 'AllowedStyles<MediaQuery>'.ts(2345)
	},
});

This is because those APIs have have a finite of possible values, and anything outside of it result in an error. Trying to introduce the generic --${string} as a key to those APIs results in TypeScript errors so it's not tenable. We need a solution that can be boiled down to a single special symbol type from a baked in CSS var API.

Describe the solution you'd like

We need a first class CSS variable API in Compiled that is the blessed way to define CSS variables, and then we can eliminate all other "hard coded" usages via lint rules and racheting. When we have a single typesafe API that we can use end-to-end we can then also teach the strict APIs about it so instead of having infinite possibilities we can teach them about the cssVar symbol so it's only a property type increase of (1).

The solution also needs to fit into the longer term strategy of how we want to handle module boundaries in the future. Right now I'm just being super naive about it.

Say we introduce a new declareVar API (could also be a map API like cssMap), It's return type would be a special tagged symbol (or string). Compiled would be updated to enable properties and values to take the value.

import { declareVar } from '@compiled/react';

export const myVar = declareVar(); // [typed-symbol], this type is unique to this specific var, plus a tagged type to make Compiled know it's a css var type to pass to the var function

Using the file path we create a hashed variable. We can use other factors to ensure uniqueness like line/column of the declaration.

export const myVar = '--_ba13s4';

Using the variable looks like this, Compiled would decide when the place -- depending on the context (yes: value, no: property).

import { var } from '@compiled/react';
import { myVar } from './vars';

const styles = css({
  color: var(myVar), // var would be a generic that tags the unique var decl type with a type that style declarations can take
  [var(myVar)]: 'red',
};

Not using the var function would be a type error as the base type of a declared var is a unique [symbol]:

import { myVar } from './vars';

const styles = css({
  color: myVar,
            ^^^^^ error
  [myVar]: 'red',
   ^^^^^ error
};

The same behaviour would apply to the strict API. Using it in XCSS prop it would look like:

import { type CSSProp } from '@compiled/react';
import { myVar } from './vars';

function MyComponent({ xcss }: { xcss?: CSSProp<typeof var(myVar), never>): JSX.Element;

<MyComponent  xcss={{ [var(myVar)]: 'red' }} />
<MyComponent  xcss={{ [myVar]: 'red' }} />
                                          ^^^^^ error

Same rules apply to the strict APIs. This needs some time to bake but these are my current thoughts.

@itsdouges itsdouges added the new feature 🆕 New feature or request label Jun 26, 2024
@itsdouges itsdouges changed the title Typed CSS variable API RFC: Typed CSS variable API Jul 2, 2024
@itsdouges
Copy link
Collaborator Author

itsdouges commented Jul 2, 2024

To make headway with this I'm going to explore a local-only variable solution next week. Basically it would block usage x-module and not work with XCSS prop, so we can progress against unsafe patterns in Jira/Confluence/Platform.

Any thoughts?

Here's an example that came up today:

const hoverActionsDisplayVar = '--hover-actions-display';

const rootStyles = css({
  [hoverActionsDisplayVar]: 'none',
  '&:hover': {
    [hoverActionsDisplayVar]: 'flex',
  }
});

const elementStyles = css({
  display: hoverActionsDisplayVar,
});

<div css={rootStyles}>
  <div css={elementStyles} />
</div>

This is all local, but the variable name is hardcoded. We can do one step better and make it hashed / typed end-to-end. Worthwhile?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
new feature 🆕 New feature or request
Projects
None yet
Development

No branches or pull requests

1 participant