facebook / stylex

StyleX is the styling system for ambitious user interfaces.
https://stylexjs.com
MIT License
8.22k stars 303 forks source link

[RFC] Defining styles inline #534

Open nmn opened 2 months ago

nmn commented 2 months ago

Describe the feature request

Motivation

It has often been suggested that it should be possible to define styles inline. There are many reasons why such a feature might be desirable:

  1. Defining styles with stylex.create is too much ceremony for one-off styles
  2. stylex.create doesn't benefit for Prop type constrains when passing styles as props.
  3. Developer preference for more co-location.

Naming Bikeshed

There has been much discussion about what the function name for this API should be.

  1. stylex.inline - Slightly verbose, slightly unclear.
  2. stylex.atom - Terse but unclear
  3. stylex.createOne - Very verbose but clear
  4. stylex.make - Short and clear, but confusing alongside stylex.create.

I would love feedback to help make a decision here. Please leave a 👍 on one of the first four comments below to register your vote.

Proposed API

Assuming the stylex.inline name, the usage would still be what you would expect from a hypothetical stylex.createOne function. i.e. Instead of defining:

const styles = stylex.create({
  base: {color: 'red'}
});

You'd be able to do:

const red = stylex.inline({color: 'red'})

Of course, it would also be possible to use this function call directly with a stylex.props() call or a component prop.

<div {...stylex.props(
  stylex.inline({color: 'red'})
)} />;

<MyButton styles={stylex.inline({color: 'red'})} />;

Since the function accepts an object, it would be possible to define a whole object of styles.

stylex.inline({
  color: 'red',
  backgroundColor: 'black',
  textDecoration: {
    default: 'none',
    ':hover': 'underline',
    ':focus-visible': 'underline',
  },
  ...
})

It would be possible to apply the styles conditionally:

<MyButton styles={[
  isRed && stylex.inline({color: 'red'}),
  isBlue && stylex.inline({color: 'blue'}),
]} />;

And it would be be possible to reduce verbosity with the import.

import {inline as _} from '@stylexjs/stylex';

<MyButton styles={[
  isRed && _({color: 'red'}),
  isBlue && _({color: 'blue'}),
]} />;

Constrains and Considerations

One of the biggest concerns when implementing this feature is dynamic styles. stylex.create enforces the usage of functions to define styles that can be dynamic. However, doing the same with the new API would be awkward.

<MyButton styles={stylex.inline((color) => ({color}))(dynamicColor)} />;

Instead, the developer expectation would be the ability to use dynamic values directly within the objects

<MyButton styles={stylex.inline({color: dynamicColor})} />;

This has two issues:

One: It is difficult to implement this correctly and detect all the ways the value of the styles may be known statically. We don't accidentally want to generate dynamic styles when the styles are actually statically know.

Two: It makes it far too easy to define dynamic styles. One of the reasons for requiring functions within stylex.create to define dynamic styles is that the vast majority of use-cases should not depend on dynamic styles. Instead conditional styles, and compositions should be sufficient. Requiring functions makes the need for truly dynamic styles explicit and enforces intentionality. Automatically generating dynamic styles with the new API would encourage patterns that would lead to bloated and slower styles.

Therefore, the current proposal would disallow the usage of dynamic styles within the new API entirely. Using dynamic values of styles within a stylex.inline() (or whatever we call it) call would be a compile-time error. It would still be possible to define constants and use them, but the amount expressivity possible would be limited by the capabilities of the compiler.

Reasons against implementing this API

There are also many great reasons why this API should not implemented. To name a few:

  1. Increased maintenance burden - A bigger API is harder to maintain takes focus away from other improvements.
  2. Increased API surface area - Makes StyleX harder to understand
  3. Leads to less readable code - Enforcing all styles be defined in stylex.create arguably creates more readable and maintainable code in the long run at the cost of some minor inconvenience upfront.
  4. The lack of dynamic styles in the API could become a pitfall.

Thoughts?

I'm posting this proposal early to gather feedback and help improve this idea before implementation.

To vote for the API name, simply react with a 👍 on one of the first four comments on this issue. For any feedback about how the API works, respond with a comment.

nmn commented 2 months ago

stylex.inline

nmn commented 2 months ago

stylex.atom

nmn commented 2 months ago

stylex.createOne

nmn commented 2 months ago

stylex.make

zaydek commented 2 months ago

This is exciting. My only hesitation with inline is that it could unintentionally lead users to believe that pseudos are not supported. Something like atom makes sense to me for this reason. Then you can think of StyleX as a composition of Style atoms. ~Either way, excited for this to land.~

necolas commented 2 months ago

The proposed API would add significant maintenance complexity and needless surface area to StyleX. We have a design principle in React which is to only have one way to do things, even if it is more verbose in some cases. Defining styles directly on an element only looks readable in the most simple of cases. We already have 3 functions to resolve styles (one is legacy) and 2 ways to merge them (include and arrays). We could be simplifying the API rather than adding more ways to do the same thing

zaydek commented 2 months ago

The proposed API would add significant maintenance complexity and needless surface area to StyleX. We have a design principle in React which is to only have one way to do things, even if it is more verbose in some cases. Defining styles directly on an element only looks readable in the most simple of cases. We already have 3 functions to resolve styles (one is legacy) and 2 ways to merge them (include and arrays). We could be simplifying the API rather than adding more ways to do the same thing

If flexibility is paramount would it not make sense to implement your concerns as ESLint warnings? The reality of not implementing inline means that users will defer to using inline styles rather than something that StyleX controls. I'd argue between those two choices, support for inline is probably preferable over users implementing inline styles themselves. Of course, users will probably still implement inline styles (unintentionally), but at least this way there's a small, incremental path to refactoring their code which is still correct.

Another implementation would be having strict mode, which disciplines developers to write code one way, like @necolas is suggesting. But not having a need for strict mode is even better.

For what it's worth I'd be willing to give up stylex.include for support for inline. I found I can lean into some kind of pattern like this (particularly with typography), when I want to compose styles:

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

export type ExtendProps = { extend?: stylex.StyleXStyles };

export function Title({ extend, ...props }: ExtendProps & JSX.IntrinsicElements["div"]) {
  return <div {...props} {...stylex.props(styles.title, extend)} />;
}

Then I can simply use <Title extend={styles.foo} />. I imagine RSD has some way of making this simpler, still.

Anyway, if StyleX makes inline or something like it a first-class primitive, I can imagine it being easily abused. A lot of people would love to implement styles orthogonal to the way they write Emotion CSS and Tailwind, which is usually more productive in the short term and easily becomes a clusterfuck of unreadable code down the road.

For this reason, the StyleSheet API, although somewhat ceremonious at times, reinforces the idea that correctness is about narrowing decisions, which I certainly appreciate. A little bit of pain up front can be worth if it if it narrows the overall complexity space of what you're trying to achieve.

@nmn Does inline also work in React Native? Another question would be, would this be possible to implement as a standalone package experiment?

nmn commented 2 months ago

The proposed API would add significant maintenance complexity and needless surface area to StyleX.

I share this concern. I have received repeated requests for something on these lines and so I'm trying to gather feedback to see if makes sense. The "Constrains & Considerations" section also describes some limitations that make me way of implementing this API.


@zaydek This RFC should not yet be considered as something we're about to land. This issue exists to gather feedback. There are still many reasons against doing this. The benefits would need to great outweigh those downsides.

if StyleX makes inline or something like it a first-class primitive, I can imagine it being easily abused.

This is another valid reason against adding this API. Although a bit subjective, I believe enforcing the usage of stylex.create for all styles results in more readable styles in the long run.

would this be possible to implement as a standalone package experiment?

This is possible and what I'm working towards. I don't want to bring this into "core" anytime soon.

Does inline also work in React Native?

Did you mean RSD? RN supports inline style objects. RSD does not support this API.

zaydek commented 2 months ago

Did you mean RSD? RN supports inline style objects. RSD does not support this API.

@nmn I actually meant RN but I didn't know RN supports inline style objects already. Also, it sounds like RSD is effectively strict mode for StyleX so that seems aligned.

Thanks for clarifying this is not 'about to land' as I assumed. FWIW everytime I've tried to write inline style-based UIs first it's always come back to haunt me. It tooks some getting used to StyleX's StyleSheet approach but honestly it's really just like CSS Modules but typed and colocated. Now it's a no brainer for me to use StyleX and I love the API. So a little friction is fine and totally valid if you're trying to do something different, which StyleX is.

Experiments help gather information, so perhaps there's some opportunity that inline unlocks, but given the choice, I think I'm indifferent. I already learned my lesson with stylex.include that I don't really need it, even though originally I thought I did. I suspect the same could be true for inline.

I do use inline styles alongside StyleX at present, but it's simply because I'm too lazy. It's probably only 2-5% so it doesn't really matter either way. I genuinely appreciate there are so few ways to do things in StyleX, as this tweet reads:

The only concepts you need to understand in StyleX:

  1. Style Objects - apply them, merge them, use them conditionally whatever
  2. Variable Objects - use them within styles
  3. Themes - set values for variables on a UI sub-tree

That’s it. Seriously.

This is very good 'north star' for StyleX.

nonzzz commented 2 months ago

I have been using stylex for a long time,To be honest, the current method is too cumbersome for react, So i mad a babel-plugin for this problem. test-case

You might notice i'm using JSXAttributes, In my opinion this is simpler to use.

inspired by style9-jsx-prop

nonzzz commented 2 months ago

AFAIK. The dynamic values are very difficult to calculate, So i have to treat each attribute as a token. But i don't think is a good way. I try to merge static token to a whole object until the token meet dynamic or SpreadElement node. But I don't know if it's worth it.

aspizu commented 2 months ago
  1. Why can't you put style objects directly in the stylex.props call?

    <div {...stylex.props({display: 'flex', flexDirection: 'column'}, ...)}>
    ...
    </div>
  2. One way this feature could be abused is by creating a library of tailwind like utility inline styles.

    
    const flex = stylex.inline({display: 'flex'})
    const flexCol = stylex.inline({flexDirection: 'column'})

<div {...stylex.props(flex, flexCol)}> ...



I feel like 2 can be solved, if only 1 is allowed. if you want to reuse styles, then you should put them in a `stylex.create` stylesheet.
nmn commented 2 months ago

Why can't you put style objects directly in the stylex.props call? @aspizu This isn't feasible for a couple of reasons:

  1. It only works for styles being applied directly on an element and not props to custom components
  2. Since conditions can be used within stylex.props detecting inline styles without a function call wrapper can be error prone.
necolas commented 2 months ago

The reality of not implementing inline means that users will defer to using inline styles rather than something that StyleX controls

Is the proposed name (inline) is confusing you (or me)? This isn't an RFC for adding what is traditionally referred to as "inline styles", we already support those using the function syntax within create().

Then I can simply use . I imagine RSD has some way of making this simpler, still.</p> </blockquote> <p>No need for a new prop name, we just pass styles down using the <code>style</code> prop.</p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/BMCwebdev"><img src="https://avatars.githubusercontent.com/u/5178086?v=4" />BMCwebdev</a> commented <strong> 2 months ago</strong> </div> <div class="markdown-body"> <p><em>removed to keep this RFC concise</em></p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/nmn"><img src="https://avatars.githubusercontent.com/u/3582514?v=4" />nmn</a> commented <strong> 2 months ago</strong> </div> <div class="markdown-body"> <p>@BMCwebdev This is a broader discussion that should live in "discussions". This issue is for discussing the possibility of adding an API for defining styles "inline" only.</p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/tounsoo"><img src="https://avatars.githubusercontent.com/u/8681348?v=4" />tounsoo</a> commented <strong> 2 months ago</strong> </div> <div class="markdown-body"> <p>I like the name <code>inline</code> but I agree that this might not be a good feature to take on at the moment - maybe in the future? I share the same concerns and also like having only one way of doing things as much as possible.</p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/stevewillcock"><img src="https://avatars.githubusercontent.com/u/1048426?v=4" />stevewillcock</a> commented <strong> 2 months ago</strong> </div> <div class="markdown-body"> <p>This is interesting. I'm not sure that it's needed but I can see it would add a bit of convenience.</p> <p>As for names, what about <code>stylex.one</code> - that seems both terse and clear (to me at least!) :)</p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/zaydek"><img src="https://avatars.githubusercontent.com/u/58870766?v=4" />zaydek</a> commented <strong> 2 months ago</strong> </div> <div class="markdown-body"> <blockquote> <blockquote> <p>The reality of not implementing inline means that users will defer to using inline styles rather than something that StyleX controls</p> </blockquote> <p>Is the proposed name (<code>inline</code>) is confusing you (or me)? This isn't an RFC for adding what is traditionally referred to as "inline styles", we already support those using the function syntax within <code>create()</code>.</p> <blockquote> <p>Then I can simply use <Title extend={styles.foo} />. I imagine RSD has some way of making this simpler, still.</p> </blockquote> <p>No need for a new prop name, we just pass styles down using the <code>style</code> prop.</p> </blockquote> <p>The name is only confusing to me because <code>stylex.inline</code> doesn't necessarily communicate that <code>stylex.inline</code> can do <em>more</em> than inline styles traditionally can (pseudos). But this is a minor point.</p> <p>I think the question to ask is, <em>is something like this:</em></p> <pre><code class="language-tsx"><div {...stylex.props(styles.foo, stylex.inline({ color: red }))} /></code></pre> <p><em>worth not having the confidence that all styles are implementing using <code>stylex.create</code>?</em></p> <p>I admit you changed my mind that having multiple ways to do the same thing is probably not worth it. You could end up with code bases that are 90% <code>stylex.create</code> or conversely 90% <code>stylex.inline</code>, which I think could be worse than just not supporting <code>stylex.inline</code> at all.</p> <p>I personally really appreciate the idea that there's only one correct way to do things. Even if it's more painful to learn the API initially, it's worth it in the long run to constrain unnecessary complexity.</p> <p>I'm curious to experience it as an experiment but not more than that for the time being.</p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/prc5"><img src="https://avatars.githubusercontent.com/u/20928302?v=4" />prc5</a> commented <strong> 2 months ago</strong> </div> <div class="markdown-body"> <blockquote> <p>Leads to less readable code - Enforcing all styles be defined in stylex.create arguably creates more readable and maintainable code in the long run at the cost of some minor inconvenience upfront.</p> </blockquote> <p>I think this can be enforced by linter setup and decided by end users if they want to block it or not. </p> <p>Maybe also some size checking if it exceeds x rows/styles then it should throw an error <code>Error: Use stylex.create for larger style collections</code>?</p> <p>There is some potential in this util for bringing custom logic in the codebases for handling some cases where we don't really need defining the variants.</p> <p>I think especially about creating ui-kits with stylex, where if you use the <code>cva</code> approach, this becomes a bit of a problem.</p> <p>For example, desired usage with cva could be:</p> <pre><code class="language-ts">const ButtonStyles = cva({ base: stylex.inline({ // ...some basic styles, that do not change }), variants: { variant: stylex.create({ solid: { // some styles }, outlined: { // some styles } // ... }), color: stylex.create({ // ... }) }, compundVariants: [ variant: "outlined" color: "primary", css: stylex.inline({ background: "transparent", borderColor: "blue" }) }] })</code></pre> <p>So in my opinion this will enable much easier and better composing of the logic, bringing greater flexibility and options for integrating it with custom logic in the codebase/libraries.</p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/nmn"><img src="https://avatars.githubusercontent.com/u/3582514?v=4" />nmn</a> commented <strong> 2 months ago</strong> </div> <div class="markdown-body"> <p>@prc5 Using <code>cva</code> with StyleX is an anti-pattern as the extra complexity isn't needed. We've covered alternatives that fit with StyleX better before.</p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/CanRau"><img src="https://avatars.githubusercontent.com/u/5196971?v=4" />CanRau</a> commented <strong> 2 months ago</strong> </div> <div class="markdown-body"> <p>Here's some reference in the docs <a href="https://stylexjs.com/docs/learn/styling-ui/using-styles/#style-variants">https://stylexjs.com/docs/learn/styling-ui/using-styles/#style-variants</a> @prc5 think that's what Naman is referring to</p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/prc5"><img src="https://avatars.githubusercontent.com/u/20928302?v=4" />prc5</a> commented <strong> 1 month ago</strong> </div> <div class="markdown-body"> <p>@CanRau Thank you reference. I used cva here as a bit of a shortcut to show that it is possible to use the given logic in some way. I myself think that since Stylex itself allows you to build various types of abstractions around it - after all, it is JS that we know well - maybe it's just fine to allow it?</p> <p>@nmn My observations are that with larger components, some logic overhead may be let's say "useful". After some time, initializing everything manually and assigning becomes just tiring, there are a lot of disconnected variables around - hence my experiments with some logic to organize it (and force it). I guess, I still haven't found the sweet spot for organizing these styles. I understand that this approach may cause unpredictable side-effects, hence the name "anti-pattern". </p> <p>I see <code>stylex.inline</code> at the same time as a method to easily abuse the way of using Stylex and the method that can increase the ease of use.</p> <p>Nevertheless, I hope that this will bring you at least a little closer to the final decision. Good luck!</p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/nmn"><img src="https://avatars.githubusercontent.com/u/3582514?v=4" />nmn</a> commented <strong> 1 month ago</strong> </div> <div class="markdown-body"> <blockquote> <p>after all, it is JS that we know well - maybe it's just fine to allow it?</p> </blockquote> <ol> <li>It's important to remember that StyleX is a compiler and does not allow arbitrary Javascript. All patterns need to static.</li> <li>Secondarily, our core principle is to keep the API simple and provide one flexible API rather than every possible API that someone might want. We're very happy with the variants patterns that can be achieved with a simple object and don't want to introduce the non-standard "cva" pattern just because it's currently a popular pattern in other styling solutions. (Objects are forever!)</li> </ol> <blockquote> <p>I guess, I still haven't found the sweet spot for organizing these styles</p> </blockquote> <p>Please open a discussion topic with your questions. In our experience using StyleX at Meta for about 5 years now, we find that while sometimes StyleX can be a bit more verbose up-front, it can be used to create readable, maintainable styling with very complex logic.</p> <p>The requirement to name styles also helps readability as you can focus on "what" rather than "how" when reading the styles of a component and focus on the logic to apply styles conditionally.</p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/zaydek"><img src="https://avatars.githubusercontent.com/u/58870766?v=4" />zaydek</a> commented <strong> 1 month ago</strong> </div> <div class="markdown-body"> <p>I wanted to follow-up because I've been thinking about this a lot. There's a lot in this but I think this is the appropriate place, here or as a discussion. In any case, this is why I think <code>stylex.inline</code> or <code>stylex.atom</code> is a very good idea for StyleX.</p> <p>At present, when implementing StyleX, you are expected to exclusively use the stylesheet API, also known as <code>stylex.create</code>. This is good because there are few ways to do things, but it can become very problematic because it requires you to step back and think about the taxonomy of your application as opposed to just building it. It's pretty obvious that libraries like Tailwind are popular because they don't force you to think, but they do lead to pretty obfuscated code that gets gnarlier the more the project grows.</p> <p><a href="https://gist.github.com/zaydek/46f1d6331efbc8d93ffda521c1b1d44d">So I did an experiment last night</a> where I attempted to combine the benefits of Tailwind and StyleX and I'm really happy with the results. This is about my process of trying to combine StyleX and utilities together and what epiphanies came from the exercise.</p> <p>In short, you can express utilities in StyleX by enumerating all the properties and values-per-property that you care about. </p> <p>For example:</p> <pre><code class="language-tsx">export const display = stylex.create({ flex: { display: "flex" }, grid: { display: "grid" }, arbitrary: (value: CSSProperties["display"]) => ({ display: value }), }); export const flexDirection = stylex.create({ row: { flexDirection: "row" }, column: { flexDirection: "column" }, rowReverse: { flexDirection: "row-reverse" }, columnReverse: { flexDirection: "column-reverse" }, arbitrary: (value: CSSProperties["flexDirection"]) => ({ flexDirection: value }), }); export const justifyContent = stylex.create({ normal: { justifyContent: "normal" }, start: { justifyContent: "flex-start" }, end: { justifyContent: "flex-end" }, center: { justifyContent: "center" }, spaceBetween: { justifyContent: "space-between" }, spaceAround: { justifyContent: "space-around" }, spaceEvenly: { justifyContent: "space-evenly" }, stretch: { justifyContent: "stretch" }, arbitrary: (value: CSSProperties["justifyContent"]) => ({ justifyContent: value }), }); export const alignItems = stylex.create({ start: { alignItems: "flex-start" }, end: { alignItems: "flex-end" }, center: { alignItems: "center" }, baseline: { alignItems: "baseline" }, stretch: { alignItems: "stretch" }, arbitrary: (value: CSSProperties["alignItems"]) => ({ alignItems: value }), }); export const gap = stylex.create({ 0: { gap: 0 }, 4: { gap: 4 }, 8: { gap: 8 }, 12: { gap: 12 }, 16: { gap: 16 }, 20: { gap: 20 }, 24: { gap: 24 }, 32: { gap: 32 }, 40: { gap: 40 }, 48: { gap: 48 }, 56: { gap: 56 }, 64: { gap: 64 }, arbitrary: (value: CSSProperties["gap"]) => ({ gap: value }), });</code></pre> <p>And then you export an object that wraps all of these utilities:</p> <pre><code class="language-tsx">export const util = { display, flexDirection, justifyContent, alignItems, gap, ... }</code></pre> <p>Then you can very easily express what would otherwise be small stylesheet objects as a composition of a few utilities.</p> <p>Here is some code I wrote last night:</p> <pre><code class="language-tsx">export function CheckboxItem() { const [checked, setChecked] = useState<Checked>("UNCHECKED"); return ( <button {...stylex.props(styles.touchableContainer, util.alignItems.center, util.display.flex, util.gap[8])} onClick={() => setChecked(toggleCheckedStart)} > <div {...stylex.props( util.backgroundColor.arbitrary(varOf(touchableBackgroundColor)), util.borderRadius[1e3], util.display.flex, util.flex[1], util.minInlineSize[0], util.paddingInlineEnd[16], )} > <div {...stylex.props(util.padding[8])}> <div {...stylex.props( util.backgroundColor.arbitrary("rgba(0 0 0 / 0.5)"), util.blockSize[16], util.borderRadius[1e3], util.inlineSize[16], )} /> </div> <div {...stylex.props(util.alignItems.center, util.display.flex, util.flex[1], util.minInlineSize[0])}> <div {...stylex.props(util.overflow.hidden, util.textOverflow.ellipsis, util.whiteSpace.nowrap)}> Hello Hello Hello Hello Hello Hello Hello Hello Hello Hello Hello Hello </div> </div> </div> <Checkbox checked={checked} setChecked={setChecked} /> </button> ); }</code></pre> <p>This is kind of mind-blowing to me. You can rename <code>util</code> to whatever you want but for all intents and purposes this is typed utility styles that is still able to piggy back on top of the StyleX compiler. It's a kind of mix-and-match approach where you can use the best tool for the job.</p> <p>This is really interesting because even values are typed and I can still opt into <code>arbitrary</code> as a fallback. What's powerful about this is that assuming <em>last still wins</em>, this is like CSS from the future where you can actually predict how things will turn out because the order means something thanks to the StyleX compiler.</p> <p>It's also interesting how long series of utils are automatically formatted as vertical lists when using Prettier, dprint, etc., which is easier to grok but also a hint to just use the style object if you find what you're doing is actually complex as opposed to just a few styles or a one-off override.</p> <p>I really enjoy this because it means that I can incrementally adopt the stylesheet API only when I truly need to compose CSS, meaning there's some story I want to tell so I name my style objects and animations and variants so as to reinforce that story. For example, naming <code>sidebar</code>, <code>sidebarAnimatingIn</code>, <code>sidebarAnimatingOut</code>. But I find that maybe as often as half the time, there is no 'story', or I don't know what that story is yet; I'm still in the process of scaffolding or reworking code or the code isn't complicated enough in the first place so as to warrant going through the trouble of creating style objects.</p> <p>So my encouragement is the following, a <code>stylex.inline</code> or <code>stylex.atom</code> API would make the following possible:</p> <ul> <li>Allow users to incrementally adopt the stylesheet API when needed</li> <li>Allow users to decompose simple style objects as a composition of utilities</li> <li>Offer feature parity with CSS-in-JS libraries that expose <code>className={css(...)}</code> as a primitive</li> <li>Offer the benefits of utility styles but with the safety and sanity of TypeScript and no extensions necessary</li> <li>Offer the ability to inline media queries and breakpoints (which my naive implementation can't do)</li> </ul> <p>As per the original argument, which was that StyleX should not adopt inline styles because it would mean there are multiple ways to do the same thing, the nuance not being said is that there are still multiple ways to name the same thing. I find the constraint of needing to name everything more annoying than having access to <code>stylex.inline</code> or <code>stylex.atom</code> for specific use-cases like experimentation, scaffolding, and one-off overrides.</p> <p>Right now, my experience with StyleX is that it makes 'hard things easy and easy things hard'. Conversely, Tailwind makes 'easy things easy and hard things hard'. In Tailwind, I can express most UI in fewer than 5 classes, but whenever I approach anything that is intrinsically complex it spirals out of control and I give up sanity and safety along the way. This is what led me to the observation that having something like utilities in StyleX, such as <code>stylex.inline</code> or <code>stylex.atom</code> or my naive <code>util</code> implementation, provides me safety and sanity from the easiest to most complex UI. I can incrementally adopt the stylesheet API as needed.</p> <p>In any case I'm personally happy that I think I have a fallback solution independent of this RFC. That being said, I see how much I personally struggle with this and I think that it would be really beneficial for the community to have a simpler way to buy in to StyleX without having to bet the farm on the stylesheet API.</p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/javascripter"><img src="https://avatars.githubusercontent.com/u/13040?v=4" />javascripter</a> commented <strong> 1 month ago</strong> </div> <div class="markdown-body"> <blockquote> <p>Defining styles directly on an element only looks readable in the most simple of cases.</p> </blockquote> <p>It's true that named styles can be more readable for larger components. However, we have the option to split them into separate, smaller components and name them appropriately to make them more readable as well.</p> <p>Named styles can improve readability for large components:</p> <pre><code class="language-jsx">const styles = stylex.create({ container: { display: 'flex', flexDirection: 'column', padding: '1rem' }, title: { fontSize: '2rem', marginBottom: '1rem' }, text: { fontSize: '1rem', lineHeight: '1.5' }, button: { backgroundColor: 'blue', color: 'white', padding: '0.5rem 1rem' }, }) function LargeComponent() { return ( <html.div style={styles.container}> <html.h1 style={styles.title}>Title</html.h1> <html.p style={styles.text}>Some text content...</html.p> <html.button style={styles.button}>Click me</html.button> </html.div> ) }</code></pre> <p>Splitting the component into smaller ones can also improve readability:</p> <pre><code class="language-jsx">function Title() { return <html.h1 style={stylex.inline({fontSize: '2rem', marginBottom: '1rem'})}>Title</html.h1> } function Text() { return <p style={stylex.inline({fontSize: '1rem', lineHeight: '1.5'})}>Some text content...</html.p> } function Button() { return <html.button style={stylex.inline({backgroundColor: 'blue', color: 'white', padding: '0.5rem 1rem'})}>Click me</html.button>; } function LargeComponent() { return ( <html.div style={stylex.inline({display: 'flex', flexDirection: 'column', padding: '1rem'})}> <Title /> <Text /> <Button /> </html.div> ); }</code></pre> <p>In practice, I use inline styles extensively in my codebase and find 3-4 levels of nesting and about 100 lines of code for each component (including inline styles) still quite manageable and readable. When things get too large or complex,splitting components often make more sense anyway because it makes it clear what parts depend on which props/state, makes it easier to reduce re-renders by memoization, etc.</p> <p>In conclusion, although I understand the concern about potential increase in complexity, I find inline styles very useful. Not all styles need naming if they are short and I find style colocation more readable in many cases.</p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/nmn"><img src="https://avatars.githubusercontent.com/u/3582514?v=4" />nmn</a> commented <strong> 1 month ago</strong> </div> <div class="markdown-body"> <p>@zaydek This may be of interest to you: <a href="https://gist.github.com/nmn/b98c21fbf3ee02c35319940b14030f62">https://gist.github.com/nmn/b98c21fbf3ee02c35319940b14030f62</a></p> <blockquote> <p>assuming last still wins</p> </blockquote> <p>It does. The main concern with this pattern is about detecting unused styles. JS tooling usually can't catch unused variables across module boundaries. I have some ideas to tackle and optimise this pattern, but it's on the back-burner for now.</p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/nonzzz"><img src="https://avatars.githubusercontent.com/u/52351095?v=4" />nonzzz</a> commented <strong> 1 month ago</strong> </div> <div class="markdown-body"> <p>In fact, I tend using <code>inline</code> to define simple styles in my component like:</p> <pre><code class="language-tsx"> import React from 'react' import { useScale, withScale } from '../../composables' interface Props { inline?: boolean } export type SpacerProps = Omit<React.HTMLAttributes<any>, keyof Props> & Props function SpacerComponent({ inline = false, ...props }: SpacerProps) { const { SCALES } = useScale() return ( <div stylex={{ width: SCALES.width(1), height: SCALES.height(1), padding: `${SCALES.pt(0)} ${SCALES.pr(0)} ${SCALES.pb(0)} ${SCALES.pl(0)}`, margin: `${SCALES.mt(0)} ${SCALES.mr(0)} ${SCALES.mb(0)} ${SCALES.ml(0)}`, display: 'block', ...(inline && { display: 'inline-block' }) }} {...props} /> ) } SpacerComponent.displayName = 'Spacer' export const Spacer = withScale(SpacerComponent) </code></pre> <p><code>JSXAtrribute</code> is same as inline function, In my option only simple and short style sheet need it. If us style sheet is big, and we need compose some short style sheet i prefer using </p> <pre><code class="language-tsx"> import * as stylex from '@stylexjs/stylex' const styles = stylex.create({ // ... a long style sheet }) function Component() { return <div {...stylex.props(styles.icon,inline({position:'static'}))}></div> } </code></pre> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/nonzzz"><img src="https://avatars.githubusercontent.com/u/52351095?v=4" />nonzzz</a> commented <strong> 1 month ago</strong> </div> <div class="markdown-body"> <p>Let me explain what i did. The <code>JSXAttribute</code> is just translating <code>JSXAttribute</code> into <code>props</code> or <code>attrs</code> and combing it with <code>create</code> In the follow code block</p> <pre><code class="language-tsx">// input const Component = () => { return <div stylex={{ color: "red" }} />; }; // output import { create, props } from "@stylexjs/stylex"; const styles = create({ xxHash: { color: "red", }, }); const Component = () => { return <div {...props(styles.xxHash)} />; };</code></pre> <p>Back to topic. In my option is that <code>inline</code> is equivalent to <code>create</code>. So i extra them to the toplevel and translate them. Here is a simple example</p> <pre><code class="language-tsx">// input import { create, props } from "@stylexjs/stylex"; const styles = create({ test: { // ... a long style sheet }, }); const Component = () => { return <div {...props(styles.test, inline({ position: "static" }))} />; }; // output import { create, props } from "@stylexjs/stylex"; const styles = create({ test: { // ... a long style sheet }, }); const _styles = create({ xxHash: { position: "static", }, }); const Component = () => { return <div {...props(styles.test, _styles.xxHash)} />; };</code></pre> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/zaydek"><img src="https://avatars.githubusercontent.com/u/58870766?v=4" />zaydek</a> commented <strong> 1 month ago</strong> </div> <div class="markdown-body"> <p>@nonzzz Is that a custom Vite plugin? What's doing the translation for you? That's really interesting.</p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/nonzzz"><img src="https://avatars.githubusercontent.com/u/52351095?v=4" />nonzzz</a> commented <strong> 1 month ago</strong> </div> <div class="markdown-body"> <p>@zaydek Yeah, I create an babel-plugin to do this. <a href="https://github.com/nonzzz/stylex-extend/tree/main/packages/babel-plugin">plugin</a></p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/zaydek"><img src="https://avatars.githubusercontent.com/u/58870766?v=4" />zaydek</a> commented <strong> 1 month ago</strong> </div> <div class="markdown-body"> <details> <summary>Amazing. Thanks!</summary> <img src="https://i.kym-cdn.com/photos/images/newsfeed/001/569/898/26c.jpg"> </details> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/zaydek"><img src="https://avatars.githubusercontent.com/u/58870766?v=4" />zaydek</a> commented <strong> 1 month ago</strong> </div> <div class="markdown-body"> <p>Just to follow up, @nonzzz did manage to implement <code>inline</code> as a Vite plugin (and it works!). I setup a simple repo to provide footing for anyone who wants to experiment with this. Exciting times.</p> <p><a href="https://github.com/zaydek/stylex-extend">https://github.com/zaydek/stylex-extend</a></p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/necolas"><img src="https://avatars.githubusercontent.com/u/239676?v=4" />necolas</a> commented <strong> 1 month ago</strong> </div> <div class="markdown-body"> <blockquote> <p>Splitting the component into smaller ones can also improve readability</p> </blockquote> <p>At the cost of performance. React components have a non-trivial performance cost. In the future, maybe the React Compiler can flatten components defined like this within a single file.</p> <blockquote> <p>It's pretty obvious that libraries like Tailwind are popular because they don't force you to think, but they do lead to pretty obfuscated code that gets gnarlier the more the project grows.</p> </blockquote> <p>It's not just that. Tailwind can lead to large amounts of "inline" class names that make the structure and behavior of the tree very hard to read, because the elements and events are lost within a sea of styles. This is less often a problem with Flutter and SwiftUI code, because they provide components for features that are part of the CSS API on web. And frankly we tend to implement more complex, responsive components on web too. Styles - like event handlers - are generally more portable and readable when they're not defined inline on elements.</p> <p>The <code>util</code> example is natively supported by StyleX with the existing APIs, and results in less JS code in bundles. I don't think the use case of quick-and-dirty prototypes justifies the complexity of adding APIs that do the same thing, often worse, than what already exists. FWIW, we've spent half a decade building all of Meta's web products with StyleX and never really had a need or request for the API proposed here.</p> <blockquote> <p><code><div stylex={{ ... }} /></code></p> </blockquote> <p>How is that going to work for custom components, which may or may not use <code>stylex</code> (or an alternative less likely to clash with the <code>stylex</code> import) as a prop name? Another issue with pretending to users that styles are just plain objects is that they might introduce runtime patterns for manipulating or reading styles that cannot be preserved when they're processed by the compiler. The type system would also struggle to cover all the cases we can currently throw errors for.</p> <p>We see a similar problem in the existing RN ecosystem, where a change to the <code>create</code> API's return value a few years ago allowed people to read style values in the render loop - it now essentially prevents potential build-time optimizations to styles being introduced, and allows for perf anti-patterns like flattening style arrays in render.</p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/zaydek"><img src="https://avatars.githubusercontent.com/u/58870766?v=4" />zaydek</a> commented <strong> 1 month ago</strong> </div> <div class="markdown-body"> <p>I took some encouragement from this reply and wanted to double down on a simple approach: <a href="https://gist.github.com/zaydek/6a3abb586c6bb82405d4f4b9c4ce7ee2">https://gist.github.com/zaydek/6a3abb586c6bb82405d4f4b9c4ce7ee2</a>.</p> <blockquote> <p>The <code>util</code> example is natively supported by StyleX with the existing APIs, and results in less JS code in bundles.</p> </blockquote> <p>If anyone's struggling with wanting to be able to more easily define inline styles in StyleX and doesn't want to depend on plugins, I've been using a simplified version of the <code>utils</code> pattern to great effect. You wouldn't want to use it everywhere because it creates CSS variables for all properties, but if there's one or two properties you'd rather not model a style object for, I find this to be very ergonomic.</p> <p>This is simpler than my previous implementation because it doesn't hard code what values you can use and defers to the <code>CSSProperties</code> type for the specified property:</p> <pre><code class="language-ts">export const u = stylex.create({ accentColor: (value: CSSProperties["accentColor"]) => ({ accentColor: value }), alignContent: (value: CSSProperties["alignContent"]) => ({ alignContent: value }), alignItems: (value: CSSProperties["alignItems"]) => ({ alignItems: value }), alignSelf: (value: CSSProperties["alignSelf"]) => ({ alignSelf: value }), alignTracks: (value: CSSProperties["alignTracks"]) => ({ alignTracks: value }), ... });</code></pre> <p>Looks like this, you probably always want to specify <code>utils</code> last.</p> <pre><code class="language-tsx"><header {...stylex.props(dialogStyles.header, u.paddingBlockEnd(0))}> <div {...stylex.props(dialogStyles.hero, u.alignSelf("center"))} /> <h1 {...stylex.props(typographyStyles.title, u.textAlign("center"))}> Are you sure you want to continue? This action is irreversible. </h1> </header></code></pre> <p>I found StyleX to be a lot easier having something like this in my back pocket, particularly for prototyping and scaffolding ideas before I'm ready to name them.</p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/nmn"><img src="https://avatars.githubusercontent.com/u/3582514?v=4" />nmn</a> commented <strong> 1 week ago</strong> </div> <div class="markdown-body"> <blockquote> <p>This is simpler than my previous implementation because it doesn't hard code what values you can use and defers to the CSSProperties type for the specified property:</p> </blockquote> <p>This is worse for performance as when you use dynamic styles with functions, you're relying on inline styles to set variables in addition to setting classNames.</p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/nonzzz"><img src="https://avatars.githubusercontent.com/u/52351095?v=4" />nonzzz</a> commented <strong> 1 week ago</strong> </div> <div class="markdown-body"> <p>A friendly ping. @nmn This RFC hasn't been updated for a long time. FWIW, I implemented it and using for a long time. I found it very useful for simple cases. Is there any further information or updates planned for the RFC?</p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/nmn"><img src="https://avatars.githubusercontent.com/u/3582514?v=4" />nmn</a> commented <strong> 19 hours ago</strong> </div> <div class="markdown-body"> <p>@nonzzz The current decision is to not implement this in StyleX itself in order to keep the API small. But the issue remains open as a central place for people to be able to chime in with their opinions.</p> </div> </div> <div class="page-bar-simple"> </div> <div class="footer"> <ul class="body"> <li>© <script> document.write(new Date().getFullYear()) </script> Githubissues.</li> <li>Githubissues is a development platform for aggregating issues.</li> </ul> </div> <script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js"></script> <script src="/githubissues/assets/js.js"></script> <script src="/githubissues/assets/markdown.js"></script> <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.4.0/build/highlight.min.js"></script> <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.4.0/build/languages/go.min.js"></script> <script> hljs.highlightAll(); </script> </body> </html>