Unify API
See original GitHub issueDescribe 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:
- The exported component can be used as a child selector for overriding styles.
- refs could be passed down without forgoing the benefits that the styled function provides
- default props (e.g. input type=“password”, button type=“button”) can be set easily. Styled Components has a seperate attr() function, but simply supplying a custom render function is a more powerful solution.
- Components with hooks can render themselves (fixing that issue) if provided as a render prop.
- shallower render trees. Exported components can include the styles required to function. No more of this:
// Styled(PasswordWrapper) This is done so that the exported module can be selected in a parent styled tag
<Input.Password>
<PasswordWrapper> {/* Root customized component */}
<ComponentRoot className="InputWrap_i17nyggi">
<div className="InputWrap_i17nyggi">
<input ... />
<span onClick={}>Show</span>
</div>
</ComponentRoot>
</PasswordWrapper>
</Input.Password>
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.
const shared = styled.fragment`
background: beige;
`
const Typography = styled.fragment`
color: black;
line-height: 1.5;
margin: 6px;
/* We can interpolate here because CSS vars are always applied to */
/* the element that composes this style */
font-size: ${props => props.size}px;
`
// Styled fragments only return class name and styles prop. Which can compose
// Then we can compose like this:
const Article = styled.article`
p {
${Typography}
}
`
const Footer = styled.footer`
${Typography}
font-size: 0.8em; /* Override what is specified in Typography */
`
const Header = styled.header`
${shared}
color: blue;
`
const Card = styled.div`
background: beige;
padding: 2em;
border-color: black;
/* Selecting child elements with the shared class name applied */
& .${shared.className} {
/* Just like any JSX styled element this styled.fragment has a className */
color: pink;
}
`
Generated output:
.Article.Typography_fragment p,
.Footer.Typography_fragment {
color: black;
line-height: 1.5;
margin: 6px;
font-size: var(--Typography_fragment_fontSize-var);
}
/* Because we are nesting and expect an override, we have to match the specificity */
.Footer.Typography_fragment {
/* The cool thing about variables here is that we are not using it here, even though it is set on footer.style */
/* It may also be possible to statically determine that the var should not be applied to <Footer /> */
font-size: 0.8em;
}
.Header {
color: blue;
}
/* a demonstration of atomization */
.shared_fragment {
background: beige;
}
.Card {
padding: 2em;
border-color: black;
/* background style removed as it is shared (as determined by compiler) */
}
/* Our override */
.Card .shared_fragment {
color: pink;
}
Generated markup:
<article
class="Article Typography_fragment"
style="--Typography_fragment_fontSize-var: '16px';"
>
<p>lorem</p>
<p>ipsum</p>
</article>
<header class="Header shared_fragment">Welcome to the site!</header>
<footer
class="Footer Typography_fragment"
style="--Typography_fragment_fontSize-var: '16px';"
>
Copyright
</footer>
<div class="Card shared_fragment">Shuffle</div>
If styled is called without an argument, fragment could be assumed. But doing so means no css syntax highlighting currently.
<button
className={cx(
styled()`
color: black;
border: red;
`,
'btn'
)}
>
Click
</button>
Step 3
Proposed syntax where styled accepts a function as its render prop:
- receives props and ref React.forwardRef
- Styled component adds classname and styles to filteredProps
- Styled component sets displayName and __linaria object on Root
- Styled helper acccepts render function (tag, filteredProps)
export const Input = styled.input`
margin: 0;
width: 100%;
height: 32px;
padding: 4px 11px;
color: rgba(0, 0, 0, 0.65);
font-size: 14px;
line-height: 1.5;
border: 1px solid #d9d9d9;
border-radius: 4px;
transition: all 0.3s;
`((filteredProps, ref, tag) => {
// I have moved tag to the end here so it can be ignored.
const { styles, className } = filteredProps
// Use hooks here etc...
return (
<div {...className} {...styles}>
<input ref={ref} {...filteredProps} />
</div>
)
})
Fragments without prop interpolations are allowed as a css replacement as such, this could be done:
<article
className={cx(
styled.fragment`
color: red;
`,
importedFragment
)}
>
Composed Styles!
</article>
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.
Issue Analytics
- State:
- Created 4 years ago
- Reactions:7
- Comments:8 (1 by maintainers)
Top GitHub Comments
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:
This is why I am not a fan of this:
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:
Modifier Classes
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: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:
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:
When compiling, linaria would transform each rule block into its own
const = Button_propsPrimary = css
call. Perhaps something like this:Why
.theme-dark & {color: white;}
but the “theme-dark” className must be manually added rather than using component state.We are currently doing everything twice.:
This repetition is unnecessary. Let’s define all styles in CSS, including reacting to props or state!
any update?