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

Style System: CSS-in-JS Workflow #2

Closed
ItsJonQ opened this issue Jul 18, 2020 · 30 comments
Closed

Style System: CSS-in-JS Workflow #2

ItsJonQ opened this issue Jul 18, 2020 · 30 comments
Assignees

Comments

@ItsJonQ
Copy link
Owner

ItsJonQ commented Jul 18, 2020

Haiii!! 👋

I've been thinking and researching a lot in various technical areas for component libraries + css. The notable topics involve performance and user experience (users being designers/devs).

Performance

Since there will be many instances of these components, it is favourable that they have high performance out-of-the-box, with little to no overhead from consumers of the library.

User Experience

The trade-off for having high-perf typically comes at a cost of the internal code being more verbose. This ultimately degrades user experience - for contributors working on the internals and for consumers of the library.

css vs styled

Without question, it appears that the (mostly) static css is more performant. There's less computation, and there are far less component wrappers. styled offers a much more fluid user experience when it comes to (automatic) prop filtering, theme context consumption, composition and more.

For this "system" idea, I've attempted to go with css.

Concept

I have a working example of this system concept. The main workflow revolves around a (singleton) system object.

From a component library workflow perspective, system offers 2 things:

  • A way to quickly access config values (e.g. color)
  • A way to render high performant (enhanced) React primitive components (e.g. system.div)
Example
import { css, system } from '@wp-g2/system';
const { get } = system;

// Defining component styles
const button = css`
	background: ${get('colorBrand')};
	border-radius: ${get('buttonBorderRadius')};
	color: ${get('colorBrandText')};
	display: inline-flex;
	font-weight: 400;
	justify-content: center;
	padding: ${get('buttonPadding')};
	user-select: none;
`;

const buttonLarge = css`
	font-size: 22px;
`;

// component library level
// uses system.base component
const Button = ({ isLarge, ...props }) => {
	return (
		<system.button
			cx={[button, isLarge && buttonLarge]}
			{...props}
		/>
	);
};

// consumer level
// overrides can happen with special css prop, enabled by system.base
const Example = () => {
	return (
		<>
			<Button css={`background: red;`} isLarge>
				Example
			</Button>
		</>
	);
};

I feel like this offers a balanced workflow!

  1. A "CSS framework" layer is not required. An example of this would be something like Taychons or Tailwind.
  2. Library level: Main difference would be using system.div vs div. Otherwise, it's mostly the same.
  3. Consumer level: Unchanged. A bonus would be the inclusion of a special css prop for ad-hoc modifications.

system.base

The system primitives are light-weight wrappers around React.createElement. They include a couple of special props that would be used internally.

  • cx : Accepts styles generated by css. Streamlines className merging with incoming props

The result is a much lighter component tree stack.

Before (using styled and BaseView)

Screen Shot 2020-07-18 at 12 05 24 PM

After (using css and system)

Screen Shot 2020-07-18 at 12 04 51 PM

system.get

The idea for get would be a hook-less way to retrieve (config) values for styling with css.

System Config

In order for hookless features like .get() to work, the system config exists as a singleton - not unlike (deep) emotion internals.

Link to Storybook experiments:
https://github.com/ItsJonQ/g2/blob/master/packages/components/src/__stories__/System.stories.js

cc'ing @diegohaz <3

@ItsJonQ
Copy link
Owner Author

ItsJonQ commented Jul 18, 2020

Leaving some more thoughts!

I think the ideal goals for a CSS-in-JS experience would be:

  • Zero config. No babel and no webpack modifications. Ideally, no Provider required.
  • As close to existing (neo)native* workflows as possible
  • Separation of styles logic from component logic. This is more apparent in larger/more complex components (like Buttons)

*Neo-native: By this, I mean CSS workflows that have been enhanced with .scss compiling and/or CSS modules

Below is a collection of thoughts based on my experiences + experiments within this area.

Feels Great

  • Sass-like nested with & is amazing. Non-negotiable.
  • Not having to think of classNames for styling. Non-negotiable.
  • Portable. Zero config. Works out of the box. Across libraries and apps. Non-negotiable.
  • The [mobile, tablet, desktop] interface for the sx prop from Theme UI.
  • Built-in functions in Sass (e.g. rgba(), darken())
  • Automatic specificity boosting by something like Styled Providers
  • Automatic transformations like RTL or CSS variable parsing/fallback injections (PostCSS workflows)

Feels Good

  • styled directly binds CSS + component. Don't need to manually connect them with className
  • styled is automatically connected to theme (provided by a ThemeProvider)
  • css is super light-weight
  • Dynamic values handled with CSS variables. Does a bunch of heavy lifting. JS no longer needs to handle logic.

Feels Ok

  • Combining css styles with cx with logic (e.g. variant === 'small')

Feels Bad

  • Complex dynamic logic in styled
  • Having to use several hooks to compose css based styles. Even having to use useTheme() feels too much.
  • Getting (and maybe) setting values in some global config. The equivalent workflow to SCSS $variable
  • Theme UI values like fontSize[2], which corresponds to the second value in the fontSize theme array. It's confusing (for me).
  • Styling with Object - especially if there are many values and/or nested values (e.g. pseudo, media queries, etc...)
  • CSS Variable values cannot be interpreted. e.g. Darkening a color value.
  • Specificity fighting. Having to do work-arounds like &&& to boost specificity
  • CSS targeting arbitrarily nested component to override styles (often a signal that the component's API/context awareness is lacking)
  • Having to order, merge, and manage className with CSS generated classNames, and passing it through

@ItsJonQ ItsJonQ changed the title "System" Concept "System" Concept - CSS-in-JS Workflow Jul 18, 2020
@ItsJonQ
Copy link
Owner Author

ItsJonQ commented Jul 18, 2020

OooOo. If we split the styles into a dedicated file, then we can have a CSS modules like workflow.

Example

// Button.styles.js
import { css, system } from '@wp-g2/system'

export const Button = css`...`

export const Content = css`...`

export const Prefix = css`...`
// Button.js
import { system as sys } from '@wp-g2/system'
import * as styles from './Button.styles.js'

const Button = ({ children, prefix, props }) => {
    return (
        <sys.button cx={styles.Button}>
            <sys.span cx={styles.Prefix}>
                {children}
            </sys.span>
            <sys.span cx={styles.Content}>
                {children}
            </sys.span>
        </sys.button>
    )
}

@diegohaz
Copy link
Collaborator

I would also add that it would be ideal to extract static CSS to static zero-runtime CSS files. I know that some solutions like linaria transform dynamic CSS into CSS variables. I would be okay with only extracting the CSS slice that is static though, and letting the dynamic styles be computed at runtime.

Do you think it's doable? I guess that would probably require some config, webpack or babel plugin. But for those who don't need zero-runtime they could just go for the zero-config solution.

Besides that, I really like the API, and I totally agree with the great, good, ok and bad points you've mentioned, especially this one:

Having to use several hooks to compose css based styles. Even having to use useTheme() feels too much.

From previous experiences, I can confirm that this results in bad DX.

Great job! ❤️

@ItsJonQ
Copy link
Owner Author

ItsJonQ commented Jul 18, 2020

Ah yes, static zero-runtime extraction. Like you had mentioned, I think a babel plugin would be required for that feature. Otherwise, it should be zero config (like styled components or Emotion.

Update~! I'm exploring the idea of a custom jsx pragma. Something that Emotion and Theme UI have.

It's not... bad, but I'd prefer to not have to do that 🙈

/** @jsx jsx */
import { jsx } from '@wp-g2/system';
import React from 'react';
import * as styles from './System.styles';

// component library level
const Button = ({ children, isLarge, ...props }) => {
	return (
		<button {...props} cx={[styles.Button, isLarge && styles.Large]}>
			<span cx={styles.Content}>{children}</span>
		</button>
	);
};

The JSX feels better vs.

import { system } from '@wp-g2/system';
import React from 'react';
import * as styles from './System.styles';

// component library level
const Button = ({ children, isLarge, ...props }) => {
	return (
		<system.button {...props} cx={[styles.Button, isLarge && styles.Large]}>
			<system.button cx={styles.Content}>{children}</system.button>
		</system.button>
	);
};

However, if it's only at the component library level. It may be okay.
I suppose the jsx pragma would be abstracted away into a babel plugin or something (for component library compiling only).

@ItsJonQ
Copy link
Owner Author

ItsJonQ commented Jul 19, 2020

I refactored some of the small primitives to system. It doesn't feel too bad~!

// packages/components/src/Text/Text.js

import { connect } from '@wp-g2/provider';
import { css, cx, system } from '@wp-g2/system';
import React from 'react';

import { Truncate } from '../Truncate';
import * as styles from './Text.styles';

function Text({
	align,
	as = 'span',
	className,
	display,
	isBlock = false,
	lineHeight = 1.2,
	size,
	truncate,
	variant,
	weight = 400,
	...props
}) {
	styles.Base = css({
		display,
		fontSize: size,
		fontWeight: weight,
		lineHeight,
		textAlign: align,
	});

	const classes = cx(
		[styles.Text, styles.Base],
		{
			[styles.Block]: isBlock,
			[styles.Destructive]: variant === 'destructive',
			[styles.Positive]: variant === 'positive',
			[styles.Muted]: variant === 'muted',
		},
		[className],
	);

	const componentProps = {
		...props,
		as,
		className: classes,
	};

	if (truncate) {
		return <Truncate {...componentProps} />;
	}

	return <system.span {...componentProps} />;
}

export default connect(Text);

Styles look like this:

// packages/components/src/Text/Text.styles.js

import { css, system } from '@wp-g2/system';
const { get } = system;

export const Text = css`
	color: ${get('colorText')};
`;

export const Block = css`
	display: block;
`;

export const Positive = css`
	color: ${get('colorPositive')};
`;

export const Destructive = css`
	color: ${get('colorDestructive')};
`;

export const Muted = css`
	opacity: 0.5;
`;

Note! Light/dark mode is built in, leveraging CSS variables to do it. I feel like this is the best way. Otherwise, the CSS-in-JS gets verbosely polluted with colour switching logic.

P.S. system does value -> CSS variable transforms for setters and getters. This implementation detail should feel invisible to folks using system.get.

@ItsJonQ
Copy link
Owner Author

ItsJonQ commented Jul 19, 2020

The vibe of this workflow is starting to resemble the spirit of Atlassian's CSS-in-JS project:
https://compiledcssinjs.com/

I recently tried to give that a shot, but I wasn't able to get the babel loader to work correctly.

It did say:

Make sure the Compiled plugin is the first plugin in the array.

That's probably where things went wrong. That feels like a pretty strong requirement though 🙈

@diegohaz
Copy link
Collaborator

Nice! ❤️

I wonder if the connect HOC is still necessary if system components resolve getters internally?

@ItsJonQ
Copy link
Owner Author

ItsJonQ commented Jul 19, 2020

@diegohaz I was going back and forth with this. I think it's still needed.

Here's my thinking!

The system.base components would be used for markup within a component (e.g. Button or Card).
connect would connect the Button or Card component to the context system.

In other words, from the library level, I think we'd only connect the root Components, rather than the (many) base markup elements within.

There may be a better way though 😊

@ItsJonQ
Copy link
Owner Author

ItsJonQ commented Jul 19, 2020

@diegohaz Continuing exploring ideas! Iterating and rolling it out to other components.

The Text component feels pretty good to me - from a DX perspective:
https://github.com/ItsJonQ/g2/blob/master/packages/components/src/Text/Text.js

I've also enhanced the base css compiler from emotion with the responsive array feature from theme-ui.

That means we can do stuff like this:

<system.div css={{
    display: ['none', 'block'],
}}>
    ...
</system.div>

It compiles down to static @media query CSS ✌️

I've opted out of Theme UI does some dynamic calculations for their responsive experience. For that, they compute values based on window.resize and window.matchMedia. From my tests, the sudden change in breakpoints causes a brief browser freeze as it re-computes and re-renders. At least, with my Stories (and how I've used the library)

https://github.com/system-ui/theme-ui/blob/1f554bb7c0078f29b5495424c8c0aa0fa5dc90ee/packages/match-media/src/index.ts

@ItsJonQ
Copy link
Owner Author

ItsJonQ commented Jul 19, 2020

I've also noticed that I haven't really used system.base elements so far. But just BaseView, like:

import { BaseView } from '@wp-g2/system';

Which follows the DX pattern of using <Box /> as your primitive, and changing the element with as.

P.S. BaseView or Box is an alias for system.div

@diegohaz
Copy link
Collaborator

I thought system components would use the context internally. So they would be already the "root" components.

@ItsJonQ
Copy link
Owner Author

ItsJonQ commented Jul 19, 2020

@diegohaz Hmm! Let's explore this :). It might be easier to have an example.

Let's say.. a Card component.

const Card = ({children, ...props}) => {
    return (
        <system.div {...props} cx={styles.Card}>
            <system.div cx={styles.Content}>
                {children}
            </system.div>
        </system.div>
    )
}

To connect it...

export default connect(Card)

The biggest benefit of the HOC is that it handles prop merging for us. Props coming from context and incoming component props.

With hooks, we'd have to do something like this:

const Card = (props) => {
    const contextProps = useConnect('Card')
    const mergedProps = {...contextProps, ...props}
    const { children } = mergedProps
    
    return (
        <system.div {...props} cx={styles.Card}>
            <system.div cx={styles.Content}>
                {children}
            </system.div>
        </system.div>
    )
}

A better version may be...

const Card = (props) => {
    const mergedProps = useConnect('Card', props)
    const { children } = mergedProps

    return (
        <system.div {...props} cx={styles.Card}>
            <system.div cx={styles.Content}>
                {children}
            </system.div>
        </system.div>
    )
}

Other thing connect does is:

  • forwardRef
  • hoist statics
  • attach special (secret) static, which the context system can use to identify other connected components

Hopefully this helps a bit!

Thank you so much for coming along for the ride!~ ❤️

@diegohaz
Copy link
Collaborator

Why does Card need access to the context?

Hmm, I was thinking about the ns prop, which I think would enable system.div to access the context by itself, right? But now I see that you've removed it from the original post. 😅

Now I also understand why it's necessary for the other reasons you mentioned.

I would rather try to avoid the name connect and go with something like createComponent, or more descriptive: createSystemComponent, createStyledComponent, createThemedComponent etc.

If we stick with the name connect, because it resembles the Redux API, I would change the API to this:

connect(namespace, options)(Component);

This function is intended to be used only internally or people consuming the library would also benefit from using this? If this is only internal, this is not really important. :P

@diegohaz
Copy link
Collaborator

diegohaz commented Jul 19, 2020

And what if connect (or another name) just received a render function instead of a component? This would be one less component to render.

const Card = createComponent({
  render({ children, ...props }) {
    return (
        <system.div {...props} cx={styles.Card}>
            <system.div cx={styles.Content}>
                {children}
            </system.div>
        </system.div>
    );
  }
})

EDIT: ref could be passed to props directly.

@ItsJonQ
Copy link
Owner Author

ItsJonQ commented Jul 19, 2020

If we stick with the name connect, because it resembles the Redux API, I would change the API to this:

Redux is where the original inspiration came from 😊

At the moment, it's used internally. However, it doesn't have to be. I'm not sure what the potential of users creating their own connected components may be (yet).

Let's go with 95% internal use for now ✌️

In that case, I think createComponent or something similar is more fitting. The render prop is interesting! I'm play around 😊 .

P.S. createStyledComponent is giving me some interesting ideas... haha.

@ItsJonQ ItsJonQ changed the title "System" Concept - CSS-in-JS Workflow Style System Concept - CSS-in-JS Workflow Jul 21, 2020
@ItsJonQ
Copy link
Owner Author

ItsJonQ commented Jul 21, 2020

This issue describes the "Style/Theme" part of the core component library systems

@ItsJonQ ItsJonQ changed the title Style System Concept - CSS-in-JS Workflow Style System: CSS-in-JS Workflow Jul 21, 2020
@ItsJonQ
Copy link
Owner Author

ItsJonQ commented Jul 22, 2020

Fun update!~ I've brought in the lower-level create-emotion package in order to inject stylis plugins features (automatically) for the css function.

https://github.com/ItsJonQ/g2/blob/master/packages/styles/src/style-system/emotion.js#L6

Plugins I've added so far are:

  • Handle CSS variable fallbacks for IE11
  • Detect/transform ltr -> rtl styles

At the moment, these plugins are naively implemented. This technique feels promising though!

I like that I no longer have to think about these things as I use the css function.
The DX feels similar to that of a project with a robust postCSS workflow ✌️

@ItsJonQ
Copy link
Owner Author

ItsJonQ commented Jul 22, 2020

Oooo! The system.get + variable + css workflow is is feeling nice!

Here's an example (from Elevation)

import { css, get } from '@wp-g2/styles';

const transition = `box-shadow ${get('transitionDuration')} ${get(
    'transitionTimingFunction',
)}`;

styles.base = css({
    borderRadius,
    bottom: offset,
    boxShadow: styles.getBoxShadow(value),
    left: offset,
    right: offset,
    top: offset,
    transition,
});

We use get() to retrieve the CSS variable associated with a key (e.g. transitionTimingFunction).

(These variable values start off as a flat object, that are transformed into a CSS variable map, and are auto-injected into the DOM)

Here's the resulting CSS output:

transition: box-shadow 200ms cubic-bezier(0.08,0.52,0.52,1);
transition: box-shadow var(--wp-g2-transitionDuration,200ms) var(--wp-g2-transitionTimingFunction,cubic-bezier(0.08,0.52,0.52,1));

Automatic variable reference and IE11 friendly CSS variable fallbacks.

🤤

@diegohaz
Copy link
Collaborator

system.get is possible because of a singleton that automatically syncs with the theme context, right?

I wonder if it will work well on server side. I say this because, on server side, multiple clients may share the same server instance:

// shared by all clients accessing this server instance
const globalArray = [];

// callback is called once for every client
app.get("/", (req, res) => {
  globalArray.push(0);
  res.send(JSON.stringify(globalArray));
});

In the example above, globalArray will be set once the server is up. And then only the callback passed to app.get() will run on every request. If 1000 people access the site while the server is up (considering there's only one instance of the server), the 1000th person will see a thousand zeros on the screen.

You can see a live example here: https://codesandbox.io/s/server-side-global-variables-rf82x?file=/pages/index.js (try to access https://rf82x.sse.codesandbox.io/ on different browsers).

This will only be a problem though if, for some reason, the theme is updated on the server (like the globalArray.push(0) above). Then, a client may affect the rendering of the next clients.

If this is a problem, system.get shouldn't be resolved statically at the module scope. Instead, it could just return a descriptor string that would be parsed later (maybe using a PostCSS plugin?) when it has access to the theme context in the React tree.

@ItsJonQ
Copy link
Owner Author

ItsJonQ commented Jul 22, 2020

Oh! I see 🤔 . That's an excellent point! I'm so glad you're able to think about these aspects 🤗 .
I've been mostly focused on the DX side at the moment.

I suppose a good question is... Is there a way values (like theme) can be reliably rendered and accessed without Context wrappers and/or hooks? Having a singleton was the only strategy I could think of.

@diegohaz
Copy link
Collaborator

diegohaz commented Jul 22, 2020

Is there a way values (like theme) can be reliably rendered and accessed without Context wrappers and/or hooks? Having a singleton was the only strategy I could think of.

I think we can use a singleton if:

  1. values will never be updated on the server;
  2. we don't need to support specific themes for subtrees.

The former is fine I guess. I've never seen that, but there may be some use cases that I'm not aware of. The latter would be easily solved by wrapping subtrees in your app within another theme context provider. With a singleton, the only solution I can think is passing another key to the getter function, but this is not the same thing.

Is it possible to resolve the getter only in React.createElement (when using the jsx pragma)?

Otherwise, I think the solution is to introduce a useCX, useClassName or something that would useContext(ThemeContext) underneath, and resolve the getter at this point.

@diegohaz
Copy link
Collaborator

diegohaz commented Jul 22, 2020

Also, another problem with the getter resolved at the module scope is that it'll only return the initial value. If the theme is updated at runtime, styles will not be affected.

So, if we want to support theme changes, we would have to resolve the getter at a later point anyway, even using a singleton. We just wouldn't need to use a hook or hook into React.createElement (jsx) to access the context. But we would probably need a hook to subscribe to the singleton changes. 😭

@ItsJonQ
Copy link
Owner Author

ItsJonQ commented Jul 23, 2020

😭 (lol)

Sanity check... Let's say, all the values from get() are CSS variables. (Let's pretend it's a requirement. That's how it works right now)

In that case, the values shouldn't necessarily matter... Correct?
Only the key matters (the CSS variable reference).

Assuming the key does not change (which it shouldn't 🤔), then we should be okay (🤞).

At the moment:

  • All key/values live in some (default) theme.js
  • These values are translated into valid CSS variables that are attached to :root on first render.
  • get() is a key to reference these CSS variables.

ThemeProvider

Let's bring in our friend Ms. ThemeProvider.

Perhaps in our system, ThemeProvider doesn't work like how it does in Emotion, Theme UI, etc.... It's used purely to adjust values from theme.js (without mutating), which renders out as CSS variables. There isn't any value passing/interpolation with the css function.

Let's say... <ThemeProvider value={{colorText: 'red'}} />

By default, the ThemeProvider could transform the theme values into CSS variables and inject it into :root {} (like the initial render).

So far so good! I think this is the typical use-case.

Scoped Variables

Let's say... We don't want to modify things at the global level. But rather... we want to modify theme things in a certain part of the app (e.g. Sidebar?).

Maybe we can do something like:

<ThemeProvider value={{colorText: 'red'}} isGlobal={false}>
  ...
</ThemeProvider>

This may render something like this:

<div data-styles-theme-provider style={{ --colorText: 'red' }}>
    ...
</div>

Components that render within can adapt the (scoped) CSS variable.

The part where this breaks... is if a component within that <ThemeProvider /> scope is portaled outside. It won't be able to absorb the CSS variable since it's no longer a child (within the DOM tree 😭 )


☝️ I recognize that all of this is quite fuzzy and abstract! Maybe I can put together a prototype (Story).

What I described is how the Styles system currently works

@diegohaz
Copy link
Collaborator

diegohaz commented Jul 23, 2020

Hmm, thanks for the explanation. I think I may have misinterpreted system.get. I thought it was just a function that would return a value from a theme object (the singleton).

Is it possible to use var(--key) instead of ${get("key")}?

@ItsJonQ
Copy link
Owner Author

ItsJonQ commented Jul 27, 2020

I thought it was just a function that would return a value from a theme object (the singleton).

That's how it originally worked! You're not wrong :).
The switch to CSS variables was a more recent development.

Is it possible to use var(--key) instead of ${get("key")}?

Absolutely ✌️ . Although, I feel like there may be some DX advantages for get() (even if it may be something like autocompleting in VSCode via types)

@ItsJonQ
Copy link
Owner Author

ItsJonQ commented Jul 28, 2020

css & styled 🤤

Haiii!! I experimented with something recently that yielded very interesting results!

That experiment being... what if we had both styled and css? Best of both worlds?

I recently added a SegmentedControl component that's based on the iOS component. Powered by Reakit goodness 👍

Screen Shot 2020-07-28 at 9 35 41 AM

This is one of the more complex components in this library (so far). There are quite a few styled moving parts (literally) to make this work.

Below, I'm going to be sharing a bit of markup used to generate a piece of SegmentedControl via several CSS-in-JS strategies we've discussed so far.

system.div

<system.div
    cx={[styles.Label, isBlock && styles.labelBlock]}
    data-active={isActive}
    >
    <Radio
        {...props}
        as={system.button}
        cx={[styles.Button, isActive && styles.buttonActive]}
        data-value={value}
        value={value}
    >
        <system.div cx={styles.ButtonContent}>{label}</system.div>
        <system.div cx={styles.LabelPlaceholder} aria-hidden>{label}</system.div>
    </Radio>
    <SegmentedControlSeparator isActive={!showSeparator} />
</system.div>

This isn't too bad. Having to prefix the HTML tag with system.div feels a little clunky. Also, as more tags add up, we start seeing a wall of system. everywhere, which makes it harder to parse.

This "wall of sameness" can definitely be felt with...

BaseView

<BaseView
    cx={[styles.Label, isBlock && styles.labelBlock]}
    data-active={isActive}
    >
    <Radio
        {...props}
        as={BaseView}
        cx={[styles.Button, isActive && styles.buttonActive]}
        data-value={value}
        value={value}
    >
        <BaseView cx={styles.ButtonContent}>{label}</BaseView>
        <BaseView cx={styles.LabelPlaceholder} aria-hidden>{label}</BaseView>
    </Radio>
    <SegmentedControlSeparator isActive={!showSeparator} />
</BaseView>

Less verbose! Using a <BaseView /> component for everything, specifying a tagName with as as needed. However, after the number of BaseView component exceeds about 2, the markup becomes really difficult to read.

The "wall of sameness" is very strong here!

styled and css

import * as styles from './SegmentedControl.styles';

const { Button, ButtonContent, Label, LabelPlaceholder } = styles;

...

<Label
    cx={[isBlock && styles.labelBlock]}
    data-active={isActive}
    >
    <Radio
        {...props}
        as={Button}
        cx={[isActive && styles.buttonActive]}
        data-value={value}
        value={value}
    >
        <ButtonContent>{label}</ButtonContent>
        <LabelPlaceholder aria-hidden>{label}</LabelPlaceholder>
    </Radio>
    <SegmentedControlSeparator isActive={!showSeparator} />
</Label>

🤤 ! Compared to the other 2, this feels a lot better to write and read! At a quick glance, we're able to understand the various pieces based on the (styled) component names! The component name indicates what the element is, and the cx prop indicates style variations.

I think this is the biggest win here in terms of DX.

How does styled work?

styled works very similarly to Emotion, Styled Components, etc... when it comes to creating styled components:

export const Label = styled.div`
	display: inline-flex;
	max-width: 100%;
	min-width: 0;
`;

Where it differs is how the component is generated.

Earlier in this thread, we favoured the static css approach for the sake of performance (which is valid). Dynamic styled components suffered due to the layers of Context consumers and series of prop resolutions.

This custom implementation of styled is basically one layer away from using a system.div primitive.
There is no Context involved.

The result is a shallow (in comparison) tree of React components:

Screen Shot 2020-07-28 at 9 52 28 AM

Limitations

This custom styled implementation is still static. It does not do prop resolution. Therefore, any dynamic variations need to be provided via the cx or className props.


All of this is still early of course! SegmentedControl was the first component this strategy was tested on. So far, it feels nice!

@ItsJonQ
Copy link
Owner Author

ItsJonQ commented Jul 28, 2020

More feedback on CSS + Styled.

Managing ClassNames

Really digging it. The only clunky bit I've noticed is having to manage cx / className hand-off + merging.

Example:
https://github.com/ItsJonQ/g2/blob/master/packages/components/src/TextInput/TextInput.js#L71

I can't pass cx into <Flex /> as it already uses it internally for it's own styles. Therefore, I have to combine classes before passing it through as className.

This problem isn't new in the CSS-in-JS space. I wish it was a little more seamless.

CSS Variables Workflow

I'm really liking the CSS variable based workflow (rather than traditional Theme context props).

It feels a lot simpler. That is, not having to do dynamic CSS value calculation or having to use a hook / context consumer.

Also, not having to manage/juggle light/dark mode values.

Theme UI attempts to streamline this via color modes.

styled Conventions

A convention I've used for a while is to suffix styled components with something. Like UI or View. For example... InputView or ContainerView rather than Input or Container.

https://github.com/ItsJonQ/g2/blob/master/packages/components/src/TextInput/TextInput.styles.js#L55

The reason for this is that it indicates to the person reading that they are looking and working with a styled component. I'm not sure if entirely matters, but I've found this distinction to be useful. To be more specific, useful in the sense of code organization + navigation.

@ItsJonQ
Copy link
Owner Author

ItsJonQ commented Aug 1, 2020

Extra Specificity (Automatically)

Oh boy! I just add a custom stylis plugin that boosts specificity of rendered styles.

That means, that (Emotion) generated styles automatically look like this:

Screen Shot 2020-07-31 at 10 35 01 PM

This greatly improves the reliability of these Components rendering into an environment with existing CSS rules.

Everything still works as expected ✌️

Note: This isn't full-proof. Existing rules that are MORE specific (e.g. prefixed with an #id) still beat these.

@ItsJonQ
Copy link
Owner Author

ItsJonQ commented Aug 6, 2020

Performance gains for @wp-g2/styles approach!

I recently recorded some profile tests to examine the computational impact of how @wp-g2/styles handles ThemeProvider updates.

Some notable points:

<ThemeProvider /> does not require any children to work

This avoids React having to drill through every component to update theme-based values!

Theme values use CSS variables

That means that the coordination/cascade and swapping is handled natively by the browser 🙏

Results:

The following screenshots are the results of my switching from dark to light theme (several times) in various component libraries (including G2), tested on production builds.

Note: Most important parts are the yellow (CPU/JS computation) and the green areas (FPS. Dips indicate dropping of frames)

G2

Screen Shot 2020-08-05 at 7 50 58 PM

Computation happens in very short bursts. Very minimal yellow. The majority of the rendering has handled by GPU (purple), which has much higher performance.

Theme UI

Screen Shot 2020-08-05 at 7 43 17 PM

Similar to G2, but with a longer "burst". Most of the work happens in yellow. Pretty good!

Material UI

Screen Shot 2020-08-05 at 7 50 10 PM

"Burst" is noticeably longer with a lot more computation in yellow.

Bumbag

Screen Shot 2020-08-05 at 7 42 23 PM

Theme switching was really heavy in this one. The stretch of yellow is quite long, and the FPS essentially drops to zero.

@ItsJonQ
Copy link
Owner Author

ItsJonQ commented Aug 27, 2020

I think the Style system has really matured! I'm closing this up :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants