Open brandonkal opened 5 years ago
I've been thinking about this a lot, so I'd be interested in hearing thoughts. It is clear to me that using a classname only approach for styling descendent selectors does introduce complexity though that is a risk a developer takes when using descendant selectors.
I do believe using classnames is the right approach. The styled component is a nice abstraction as you don't have to think about classnames but with the exception of the styles for variables, they really do the same thing:
const Button = styled.button`
color: red;
`
<Button />
<button className={css`
color: red;
`} />
This is why I am not a fan of this:
const ButtonGroup = styled.div`
${Button} {
margin: 0;
}
/* vs */
.${Button} {
margin: 0;
}
`
Two different ways to select an applied className. You have to look at the implementation of Button to determine if a leading dot should be used. Both methods do the same thing from the application developer's perspective. It appears the reason for the difference is that the first came from styled components which chose the first. The second came from emotion because emotion's css function used to return a classname.
Pros for using styled:
Pros for using css:
Astroturf supports arbitrary modifier classes defined in the css function. This creates a problem when you wrap components: https://github.com/callstack/linaria/issues/234#issuecomment-426421254.
One solution is to use an arbitary prefix such as $
. I don't like this approach because then there is no easy way to also pass through these properties. Also, JSX may eventually support something like this: <Box ${modifier} />
which looks too similar. I would propose to instead determine if a prop should pass through where it is defined. After all, we can think of our classes as functions of state:
const Button = styled.button`
color: black;
border: 1px solid black;
background-color: white;
&[props|primary] {
color: blue;
border: 1px solid blue;
}
&[props|color=green--] {
color: green;
}
`;
<Button primary color="green">
I am using the props namespace here to make it clear that these are functions of props. It feels more like CSS, but the following could work with some work:
${props => (props.primary && generateClassName(&, css.fragment`
color: blue;
border: 1px solid blue;
`), true)}
where generateClassName is a function that takes a prefix and a css string.
Because rule blocks are a function of state, we can pass three arguments in the CSS:
So above, the first rule has the condition that props.primary is truthy. The --
suffix tells styled to not pass through the property. Props are passed through to custom components by default (unless the render function it is a styled DOM node where valid props are known). Simply include a --
to opt out of this for a specific prop.
This avoids something like this:
<MyStyledButton $primary primary />
When compiling, linaria would transform each rule block into its own const = Button_propsPrimary = css
call. Perhaps something like this:
function css(prefix = "", suffix = "", stateCondition, passThrough = true)`rule` {}
const ButtonBase = css()`
color: black;
border: 1px solid black;
background-color: white;
`
const Button_PropsPrimary = css(ButtonBase, "", (props) => !!props.primary, true)`
color: blue;
border: 1px solid blue;
`
const Button_PropsColorGreen = css(ButtonBase, "", (props) => props.color === "green", false)`
color: green;
`
const ButtonClassName = cx(ButtonBase, Button_PropsPrimary, Button_PropsColorGreen)
<button className={ButtonClassName}>
.theme-dark & {color: white;}
but the "theme-dark" className must be manually added rather than using component state.We are currently doing everything twice.:
const buttonBase = css`
color: black;
background: white;
padding: 5px;
`
const buttonPrimary = css`
background: blue;
color: white;
`
let buttonSize = {}
buttonSize.large = css`
font-size: 3em;
`
function Button({ size, primary, ...props}) {
const styles = cx([
buttonBase,
primary && buttonPrimary,
size && buttonSize[size] && buttonSize[size]
])
<button className={styles} {...props} />
}
This repetition is unnecessary. Let's define all styles in CSS, including reacting to props or state!
I believe I've found a better way to unify these APIs. This takes advantage of Linaria being compiled at build time. Steps:
const spacing = styled`
margin-right: props.margin;
`.css() // props could be taken from render function closure if not passed as argument
...
<MenuItem>
<Link className={spacing.className}><Icon /></Link>
<span>text</span>
</MenuItem>
This needs some more thought. This would allow using all the features of styled() when required without necessarily being tied to rendering a specific element.
Requesting a response.
Hey @brandonkal, is this issue still up-to-date? I'd like to comment, but I just want to make sure you haven't gone beyond where your last message left off. I've got a fairly complex project that I proof-of-concepted with Linaria, and while I've hit some snags, I'd like to stick with it. So I'd very much like to offer feedback (and perhaps code in the future).
Hello. I haven't thought of this for some time but still believe the premise is valid.
I just want to chime in and say this is a fantastic summary of the nuances involved with the css
and styled
API's for Linaria and most other CSS-in-JS libraries. I've encountered these same issues time and again but never wrote them out so concretely. I would fully support any effort to create a unified API, as @brandonkal suggests.
I'll have to read this a few more times to totally grok it and offer feedback.
Is there a workaround for this in the meantime? Is this proposal still being considered or some other kind of implementation?
any update?
Describe the feature
CSS in JS libraries are maintaining two seperate APIs, namely styled and css. There are good reasons for both, but in react projects, it is an unnecessary complication. I wish to show that with Linaria's approach it is possible to get the best of both (composition, selection, prop interpolation, and automated css variable interpolations)!
Problem
Having a single consistant and powerful API is important. Using both together creates some edge cases. Exclusively using the current styled API can cause certain components (those that use Hooks) to not render and you quickly end up creating many components with deep nesting. An example of this is an input with a "clear" button. It must be wrapped in a div to contain and position the button but a styled component would cause the ref to refer to the parent div rather than the input as expected.
For this reason and others, Styled Components are also not currently suitable for building more complex components such as those found in UI libraries. It can be done, but it results in deep nesting. Because Linaria's styled runtime size is near zero, Linaria is in a unique position to unify these two approaches.
Proposal
Step 1
The first step would be to support a render prop on the styled component, this would solve many issues with building reusable components:
This is a simple example, but it can get worse, e.g. the component consumer then wraps the component to customize the styles, or more children use styled. It was worse with emotion, where everything with a css prop was automatically wrapped in multiple Context.Consumers.
The styled function provides a lot of utility and adding a render prop would eliminate a case where css must be used instead.
Step 2
Introduce the styled.fragment. The styled API would also change slightly so that it can be used as a replacement for the css function in every case. The babel plugin could optionally would rewrite styled to css where appropriate though this is not required. This solves the composition issue with styled.
Fragments support interpolation and cannot be used alone if they contain references to component props. When called, they return their contents. This is unlike css, which returns a className. They do generate unique classnames. This allows elements that compose this element to be selected. To access the fragment's generated classname directly, call its className property.
Generated output:
Generated markup:
If styled is called without an argument, fragment could be assumed. But doing so means no css syntax highlighting currently.
Step 3
Proposed syntax where styled accepts a function as its render prop:
Fragments without prop interpolations are allowed as a css replacement as such, this could be done:
Related Issues
This should fix: https://github.com/callstack/linaria/issues/244 and https://github.com/callstack/linaria/issues/234 plus https://github.com/callstack/linaria/issues/418.