-
Notifications
You must be signed in to change notification settings - Fork 12
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
Comments
Leaving some more thoughts! I think the ideal goals for a CSS-in-JS experience would be:
*Neo-native: By this, I mean CSS workflows that have been enhanced with Below is a collection of thoughts based on my experiences + experiments within this area. Feels Great
Feels Good
Feels Ok
Feels Bad
|
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>
)
} |
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:
From previous experiences, I can confirm that this results in bad DX. Great job! ❤️ |
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 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 refactored some of the small primitives to // 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. |
The vibe of this workflow is starting to resemble the spirit of Atlassian's CSS-in-JS project: I recently tried to give that a shot, but I wasn't able to get the babel loader to work correctly. It did say:
That's probably where things went wrong. That feels like a pretty strong requirement though 🙈 |
Nice! ❤️ I wonder if the |
@diegohaz I was going back and forth with this. I think it's still needed. Here's my thinking! The 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 😊 |
@diegohaz Continuing exploring ideas! Iterating and rolling it out to other components. The I've also enhanced the base That means we can do stuff like this:
It compiles down to static I've opted out of Theme UI does some dynamic calculations for their responsive experience. For that, they compute values based on |
I've also noticed that I haven't really used
Which follows the DX pattern of using P.S. |
I thought system components would use the context internally. So they would be already the "root" components. |
@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
Hopefully this helps a bit! Thank you so much for coming along for the ride!~ ❤️ |
Hmm, I was thinking about the Now I also understand why it's necessary for the other reasons you mentioned. I would rather try to avoid the name If we stick with the name 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 |
And what if const Card = createComponent({
render({ children, ...props }) {
return (
<system.div {...props} cx={styles.Card}>
<system.div cx={styles.Content}>
{children}
</system.div>
</system.div>
);
}
}) EDIT: |
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 P.S. |
This issue describes the "Style/Theme" part of the core component library systems |
Fun update!~ I've brought in the lower-level https://github.com/ItsJonQ/g2/blob/master/packages/styles/src/style-system/emotion.js#L6 Plugins I've added so far are:
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 |
Oooo! The Here's an example (from 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 (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. 🤤 |
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, 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 If this is a problem, |
Oh! I see 🤔 . That's an excellent point! I'm so glad you're able to think about these aspects 🤗 . I suppose a good question is... Is there a way values (like |
I think we can use a singleton if:
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 Otherwise, I think the solution is to introduce a |
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 |
😭 (lol) Sanity check... Let's say, all the values from In that case, the values shouldn't necessarily matter... Correct? Assuming the key does not change (which it shouldn't 🤔), then we should be okay (🤞). At the moment:
ThemeProviderLet's bring in our friend Ms. Perhaps in our system, Let's say... By default, the So far so good! I think this is the typical use-case. Scoped VariablesLet'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 ☝️ 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 |
Hmm, thanks for the explanation. I think I may have misinterpreted Is it possible to use |
That's how it originally worked! You're not wrong :).
Absolutely ✌️ . Although, I feel like there may be some DX advantages for |
css & styled 🤤Haiii!! I experimented with something recently that yielded very interesting results! That experiment being... what if we had both I recently added a 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
|
More feedback on CSS + Styled. Managing ClassNamesReally digging it. The only clunky bit I've noticed is having to manage Example: I can't pass This problem isn't new in the CSS-in-JS space. I wish it was a little more seamless. CSS Variables WorkflowI'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 ConventionsA convention I've used for a while is to suffix styled components with something. Like The reason for this is that it indicates to the person reading that they are looking and working with a |
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: 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 |
Performance gains for
|
I think the Style system has really matured! I'm closing this up :) |
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
vsstyled
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:color
)system.div
)Example
I feel like this offers a balanced workflow!
system.div
vsdiv
. Otherwise, it's mostly the same.css
prop for ad-hoc modifications.system.base
The
system
primitives are light-weight wrappers aroundReact.createElement
. They include a couple of special props that would be used internally.cx
: Accepts styles generated bycss
. StreamlinesclassName
merging with incoming propsThe result is a much lighter component tree stack.
Before (using
styled
andBaseView
)After (using
css
andsystem
)system.get
The idea for
get
would be a hook-less way to retrieve (config) values for styling withcss
.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
The text was updated successfully, but these errors were encountered: