cssculpt v0.4.11
We build our components in hierarchical trees, like nested react components. Why not define their styles in the same way?
When I write CSS, I use it to style a component. A component has properties, and states. Simple enough... but what happens
when we have, say, a .btn
style with 7 sizes, 4 colors, and then multiple states like :hover, disabled, etc. They all start conflicting—things
get hard to reason about. That's what's so awesome about react; it makes things very simple.
Lets apply some of the concepts from stateless components && the HOC pattern to building CSS.
Example
disclaimer: all of the following examples are purely hypothetical right now. Although there's already a lot of code written,
cssculpt
is still in its infancy. This is what it may look like:
import { sculpt, kiln } from 'cssculpt';
import { Color, darken } from 'cssculpt/chroma';
const buttonSculpture = sculpt('.button', (props, node) => {{
color: props.textColor,
/** optional destructuring for appropriate css rules **/
font: {
weight: 500
},
borderRadius: '2px',
border: `1px solid ${darken(props.baseColor, 0.4)}`, // SCSS like utility functions a plenty
/**
* the next line is an example of some shorthand; without a unit (eg: '4px')
* it defaults to px -- and you'll be able configure that default unit
**/
padding: [4, 2],
background: props.baseColor,
'&:hover': { // :pseudo selectors, @atrules welcome
background: darken(props.baseColor, 0.3)
}
});
// render the style with a given set of props
const buttonStyle = kiln.fire(buttonSculpture, {
baseColor: '#ccc',
textColor: Color.black
});
const styleSheet = StyleSheet();
styleSheet.add(buttonStyle);
console.log(styleSheet.getCSS());
Console Output:
{
'.button': {
'color': '#000',
'font-weight': 500,
'border-radius': '2px',
'border': '1px solid #666'
'padding': '4px 2px',
'background': '#ccc',
},
'.button:hover' {
'background': '#808080'
}
}
Nothing revolutionary, so far... Here's where things get interesting.
Normally with css. If we wanted to make a small variation on this, like say a button that was blue, we would have
to manually go through and create new declarations under rules that have more specific selectors;
targeting only a subset eg: .button.button-blue
. On the surface that's not that difficult. We've been doing it for
years, you're used to it.
But what about the :hover
state? You need to specify a background for .button.button-blue:hover
.
Again not that hard, but say you have an entire pallet of colors, and you decide that :hover
should be a little darker.
Following the traditional convention, you now have to go back and tweak those values, for every color on the pallet.
That's a pain in the ass. Imagine we could do this:
const colors = { red: '#ff0000', green:'#00ff00', blue: '#0000ff' /** orange, indigo, etc.. */};
const coloredButton = buttonStyle.mold((props, parentProps, node) => {
return node.parent({ ...parentProps, ...props });
});
const colorfulButtons = colors.map((v, k) => {
return coloredButton.recast(`&.button-${k}`, { baseColor: v });
})
Okay, so this is where it starts to get fun. Let me explain what is happening:
buttonStyle.mold()
takes one parameter, a callback that looks like:mold(props: Object, parentProps: Object, node: TreeNode)
props
is the object that will be passed when callingcoloredButton.recast
parentProps
is the props object that was used bykiln.fire
to createbuttonStyle
node.parent(props: Object)
this calls the "render" method we created forbuttonSculpture
withsculptNode
waaayyyy back at the begining. Its returns a newStyleTree
.
So basically we "re-render" the original sculpture, but we merge our new props from
recast
props into the original set of props passed by `kiln.fire. This way the rendering logic remains the same, but uses the new set of values. This is the simplest example of whats possible! Just using a different set of props here; later on we'll start merging in custom rules and more.for each color, we compute a new style by calling:
> `recast(selector: String, props: Object)` We specify a new root selector for this component for each color. They'll look like: `.button.button-red`, `.button.button-green`, etc. But the key part here is: those rendered variations are diffed against the original `buttonStyle` and values which are the same are omitted. And yes, it takes `:hover` and anything else nested in there into consideration too!
`
So if we spat that out into css, this would be the result
{
/** ...same output from the first example... **/
'.button.button-red': {
'background': '#ff0000',
'border': '1px solid #99000'
},
'.button.button-red:hover': {
'background': '#b30000'
},
'.button.button-green': {
'background': '#ff0000',
'border': '1px solid #009900'
},
'.button.button-green:hover': {
'background': '#00b300'
}
/** ...etc, etc... **/
}
Sure this could be achieved with some complex, temperamental SASS/LESS mixins, but this is just the begining of what cssculpt
can accomplished.
Contributions
Thanks for reading! I'm working hard on this because I think it has the potential to be huge. If you have any feedback at all, please open an issue. This is a pretty ambitious project, so if anyone wanted to join the effort, in any capacity, I would be greatly humbled.
Roadmap
the sky ain't no limit
There's so much that could be added to this, so much that is possible; here's just a few things:
- crazy nesting with custom syntax
{ .Button { ...buttonStyles & i.icon { ...iconStyles ^:hover & { // .Button:hover i.icon -- keep dom elements nested in one tree, let them target states on the parent ...iconStylesOnButtonHover } } &:hover { ...buttonHoverStyles } } }
- Composing styles
- Wide range of helper functions like
darken()
- maybe even
node.border('1px', Border.Solid, Color.red)
in aNodeSculptor
(aka that arrow function given tosculpt()
andmold()
in the example
- maybe even
- drop-in
postcss
support (autoprefixing, and more) react
binding via classnames (similar tocssmodules
)- plugins for
webkit
,gulp
,grunt
- many more