cssculpt
v0.4.11
Published
Imagine React's Stateless Components... but for css. Create a style and then use the HOC pattern to generate variations on those styles
Readme

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,
cssculptis 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)propsis the object that will be passed when callingcoloredButton.recastparentPropsis the props object that was used bykiln.fireto createbuttonStylenode.parent(props: Object)this calls the "render" method we created forbuttonSculpturewithsculptNodewaaayyyy back at the begining. Its returns a newStyleTree.
So basically we "re-render" the original sculpture, but we merge our new props from
recastprops 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 originalbuttonStyleand values which are the same are omitted.And yes, it takes
:hoverand 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
postcsssupport (autoprefixing, and more) reactbinding via classnames (similar tocssmodules)- plugins for
webkit,gulp,grunt - many more
