RFC composable class names
See original GitHub issueMotivation & Examples
Currently css
returns a single string with all class names, so it isn’t possible to get a single class name for a specific property. For example:
const classNames = css({ color: 'blue', background: 'red' }) // e.g output: "c0 c1"
classNames.color // <<< this isn't possible
It’d be great if the result of css
was like:
{
color: 'c0',
background: 'c1',
toString() { return 'c0 c1' },
}
An example use case:
const styles = css({ color: 'blue', background: 'red' })
function MyComponent() {
return (
<div className={styles.background}>
<ChildComponent className={styles.color} />
</div>
)
}
This would offer many benefits:
- As a user, you could avoid using a selector to style children (e.g:
& > div
) and just pass down class names as props. In this case, a child component that is re-usable but it should be styled differently depending on where it is used. For example, a re-usableHeading
component which could have different color. - It’d make it easier to create a Webpack plugin/loader to extract static styles #37 because. For example: Static analysis is difficult or impossible
// A.js
const ComponentA = ({ color }) => <div className={css({ color })} /> // What is color??
// B.js
const ComponentB = () => <ComponentA color="#fff" />
However, this is better and makes it possible to make static analysis of the code:
// A.js
const ComponentA = ({ color }) => <div className={color} />
// B.js
const styles = css({ color: '#fff' }) // Static analysis is possible
const ComponentB = <ComponentA color={styles.color} />
Details
stylex
was shown in ReactConf by Facebook, so the original idea comes from there:
const styles = stylex.create({
blue: { color: 'blue' },
red: { color: 'red' },
})
function MyComponent() {
return (
<span className={styles('red', 'blue')}>I'm blue</span>
)
}
Based on that:
css
could return an object instead of serializing all classnames:
const styles = css({
color: 'hotpink',
backgroundColor: 'red',
selectors: {
'&:focus': { color: 'green' },
'&:hover': { color: 'lime' },
},
})
// Returned value should look like:
const styles = {
color: 'c0',
backgroundColor: 'c1',
selectors: {
'&:focus': { color: 'c2' },
'&:hover': { color: 'c3' },
toString() { return 'c2 c3' },
},
toString() { return 'c0 c1 c2 c3' },
}
This could allow to access any classname:
styles.toString() // c0 c1 c2 c3
styles.color // c0
styles.selectors.toString() // c2 c3
styles.selectors['&:focus'] // c2
Note: also add valueOf()
? 🤔
otion
could export acreate
method (or choose better name) that could allow to create a styles object. This could basically just be (overly simplified version):
function create(styleObj: Record<string, CSSPropsThing>) { // << Generic type is better
const keys = Object.keys(styleObj)
const styles = {}
keys.forEach(k => styles[k] = css(styleObj[k])) // << Take into account nested stuff (selectors)
styles.toString = function() { return '....' /* get all classnames somehow */ }
}
So, similarly as with css
:
const styles = create({
square: {
color: 'hotpink',
border: '1px solid blue',
},
})
// Returned value should look like
const styles = {
square: {
color: 'c0',
border: 'c1',
toString() {...}
},
toString() { return combineAllStyles(...) } // See next 👇
}
otion
could export acombine
method (or choose better) to combine style objects and deduplicate styles:
const style1 = css({ color: 'red', background: 'blue' }) // { color: 'c0', background: 'c1' }
const style2 = css({ color: 'blue' }) // { color: 'c2' }
const finalStyle = combineOrDedupeOrFancyName(style1, style2)
// Return should be
const finalStyle = { color: 'c2', background: 'c1' } // color is blue, background is blue
TL;DR It’s a breaking change but I believe this could allow for:
- Easier static analysis, so a babel plugin would be easier to write (see: https://github.com/giuseppeg/style-sheet)
- It allows for composable class names that could be passed down as props in React apps (or similarly outside of React) and can potentially allow the user to avoid writing complex selectors that style children
Summarizing how the new API could look like and it’s usage:
// A.js - unchanged (because `toString()`)
const MyComponentA = () => (
<div className={css({ color: 'red' })} />
)
// B.js - `toString` of `create` return would combine & serialize all class names
const MyComponentB = () => (
<div className={css.create({
box: { border: '1px solid red' },
other: { color: 'blue' }
})} />
)
// C.js - composable
const MyComponentC = ({ className }) => <div className={className} />
const styles = css.create({
box: { border: '1px solid green', background: 'grey' },
child: { color: 'blue' },
})
const MyComponentD = ({ inheritStyles }) => (
<div className={css.combine(styles.box, inheritStyles)}> // < Combine own styles & optionally inherit from props
<MyComponentC className={styles.child} /> // pass down single class name
</div>
)
const sectionStyles = css.create({
div: { ... }
box: { border: '2px solid lime' },
})
const MyComponentE = () => (
<div className={sectionStyles.div}>
<MyComponentD inheritStyles={sectionStyles.box} /> // <<< override MyComponentD `border`
</div>
)
Let me know what do you think 😅 and if it’s within the scope of otion
to support this. I can help with the implementation 😄
Issue Analytics
- State:
- Created 3 years ago
- Comments:6 (5 by maintainers)
Top GitHub Comments
Thank you for sharing your thoughts in detail, @etc-tiago! While object spreading is possible, someone may decide to pass stringified class names generated by otion to a component, e.g.:
For this case, otion should keep track of its injected rules in a class-keyed Map. When composing an object with a list of class name strings, the latter should be mapped back to objects and then deep-merged with the initial object. I think the library should be re-architectured to accommodate room for implementing this functionality.
As much as I don’t see a use in my current cases, I believe the proposal is quite interesting. So my proposals for changes if you choose to implement them are:
Before implementation, separate otion into 3 packages:
otion-core
- the logic behind the generation of classesotion-injectors
- with injectors in htmlotion
- the combination of otion-core and otion-injectorsAfter the separation, implement a fourth package, callend
otion-compose
with the proposed changes.It could also be called
@otion/core
,@otion/css
and@otion/combine
if separation is an option.why: I think the package size can increase considerably if everything is added in a package and otion would have two directions, one to generate classes as currently and the other to create combined classes
Tell me what you think about it.