Write modular, composable CSS in a functional way with pure javascript.
-
Try mss with online compiler.
-
usage:
- npm install mss-js and
var s = require('mss-js');
- clone this repo and
var s = require('mss.js');
- add a script tag and
var s = window.mss
- npm install mss-js and
MSS.parse is a extremely small library to compile a javascript object to a css string.
-
A input object is a plain javascript object, usually nested.
-
A key will be a CSS selector if its value is another mss object, otherwise it will be taken as a property name for CSS.
-
MSS will parse the nested object into nested selectors by connnecting selectors into a descendant selector.
Let's see an example:
var s = require('mss.js');
var myStyle = {
FooClass: {
zIndex: 999,
$BarId_AnotherClass: {
MozBoxShadow: '10px 10px 5px #888',
input: {
borderRadius: '12px',
padding: '12px',
width: '100%'
}
}
}
};
// set second argmument to true to enable prettify
console.log(s.parse(myStyle, true));
// insert the style into DOM's <head>
s.tag(myStyle)
will yield:
.FooClass #barId input,
.FooClass .AnotherClass input{
border-radius:12px;
padding:12px;
width:100%;
}
.FooClass #barId,
.FooClass .AnotherClass{
-moz-box-shadow:10px 10px 5px #888;
}
.FooClass{
z-index:999;
}
As shown above, the selectors and prop name are converted for the writing ease in Javascript(using valid variable as much as possible). As a coffeescript user, i can write the example above like this:
s = require 'mss'
myStyle =
FooClass:
zIndex: 999
$BarId_AnotherClass:
MozBoxShadow: '10px 10px 5px #888888'
Input:
borderRadius: '12px'
padding: '12px'
width: '100%'
Much nicer! Following example will be written in coffee, really sorry for the inconveince, but coffeescript's object literal is much nicer to read and write.
-
Class selector is written in
UpperCase
and turn to.UpperCase
. -
html tag selector is written in
lowerCase
and turn tolowerCase
. -
id selector is written in
$UpperCase
and turn to#lowerCase
.
mss
Slider:
margin: ...
canvas:
margin: ...
$Index:
margin: ...
css
.Slider { margin: ...}
canvas { margin: ...}
#index { margin: ...}
$
is used to write pesudo-class selector:
mss
SliderBtn:
padding: ...
$hover:
color: ...
css
.SliderBtn { padding: ... }
.SliderBtn:hover { color: ... }
_
turn selector into a list of selectors, and you can nest list
mss
BlueBird_BlueOcean:
color: 'blue'
RedText_$RedWine_span:
color: 'red'
css
.BlueBird,
.BlueOcean {
color: blue;
}
.BlueBird .RedText,
.BlueBird #redWine,
.BlueBird span,
.BlueOcean .RedText,
.BlueOcean #redWine,
.BlueOcean span {
color: red;
}
- turn
camelCase
tocamel-case
andMyCamelCase
to-my-camel-case
mss
FooBar:
marginLeft: '50%'
MozBoxShadow: '10px 10px 5px #888888'
css
.FooBar {
margin-left: 50%;
-moz-box-shadow: 10px 10px 5px #888888;
}
- You can use
mss.merge
to merge an array of mss into a single mss object, it will merge all the elements from the first to last order, so the last one comes with the hightest precedence, this's very handy when you want to compose nested mss.
mss
otherMss =
OtherWidget:
margin: '2px'
Foo: mss.merge [
padding: '10px'
Bar:
color: '#ddd'
,
otherMss
]
css
.Foo{
padding: 10px;
}
.Foo Bar{
color: '#ddd';
}
.Foo .OtherWidget{
margin: '2px';
}
-
Number values can be use as property values, they will be converted to string, eg.
margin: 0
will be parsed asmargin: '0'
, so if you want to add unit, write string literals like'2px'
or'10%'
. -
While the selector's syntax covered most of the cases in basic modular css design, You can alway fallback to write a real css selector using string literal as key:
mss
'.css-selector:nth-child(2)':
background: '#ff0000';
css
.css-selector::nth-child(2){
background: #ff0000;
}
- Selectors begin with
@
are float to global space to match css@rules
, forMediaQuery
andKeyFrames
see here:
mss
Foo:
padding: '0px'
'@media all and (max-width:1200px)':
Content:
width: '960px'
css
.Foo{
padding:0px;
}
@media all and (max-width:1200px){
.Content{
width:960px;
}
}
Here comes the fun part, since the plain mss object you write is actual a simplified CSS AST, so all functions and mixins can be written in plain javascript.
Since all property are strings, we can write functions to help us produce property string, for example:
mss
px = (x) -> x + 'px'
pc = (x) -> x + '%'
$OhMyGod:
margin: px 2
padding: pc 10
css
#ohMyGod {
margin: 2px;
padding 10%;
}
There's some functions built-in with mss, they are written in camelCase, such as px
or hsl
, and often they are quite general, for example the built-in px
function can take multiple arguments, here's built-in list:
num # num('2px') == 2
unit # unit('2%') == '%'
px # px(1, 2, 3) == '1px, 2px, 3px'
pc # pc(1, 2, 3) == '1%, 2%, 3%'
gold # px(v) == Math.round(v*0.618), golden ratio caculator
goldR # px(v) == Math.round(v/0.618), golden ratio caculator 2
rgb # rgb(0, 128, 255) == 'rgb(0,128,255)'
rgba # rgba(0, 128, 255, 0.2) == 'rgba(0,128,255,0.2)'
bw # bw(128) == 'rgb(128,128,128)'
hsl # same as rgb, h: 0~360, s: 0~100, l: 0~100
hsla # same as rgba, h: 0~360, s: 0~100, l: 0~100 a: 0.0~1.0
Mixins are special functions, they should take some parameters(or none if don't need), then return a function that modify mss objects, let's write one:
mss
Center$ = (mss) ->
mss.textAlign = 'center'
mss
Padding = (pad) -> (mss) ->
mss.padding = pad
mss
input: Center$ Padding('2px')
margin: 0
css
input{
margin: 0;
padding: '2px';
text-align: 'center';
}
The mixins are totally composable because they have same type: take a mss, return a modified mss back, if you choose to write javascript, take care of the nest function application carefully:
var Center$ = function(mss) {
mss.textAlign = 'center';
return mss;
};
var Padding = function(pad) {
return function(mss) {
mss.padding = pad;
return mss;
};
};
{
input: Center$(Padding('2px')({
margin: 0
}))
}
Here's some built-in mixins with their equivalent mss:
# Vendor add vendor prefix to given property
Foo: Vendor('borderRadius')
borderRadius: '2px'
Foo:
mozBorderRadius: '2px'
WebkitBorderRadius: '2px'
MsBorderRadius: '2px'
borderRadius: '2px'
# Mixin mix another mss object
Foo: Mixin(padding:'2px')
otherProp: '...'
Foo:
padding: '2px'
otherProp: '...'
# Size are shorthand for width and height
Foo: Size('200px', '100%')
otherProp: '...'
Foo:
width: '200px'
height: '100%'
otherProp: '...'
# PosAbs set position to absolute and set top, right, bottom, left in arguments order.
Foo: PosAbs('10px', '10px', 0, 0)
otherProp: '...'
Foo:
position: 'absolute'
top: '10px'
right: '10px'
bottom: 0
left: 0
otherProp: '...'
# PosRel is just the same with PosAbs, except the position is set to 'relative'
Foo: PosRel('10px', '10px', 0)
otherProp: '...'
Foo:
position: 'relative'
top: '10px'
right: '10px'
bottom: 0
otherProp: '...'
# LineSize set height = lineHeight = first argument, and fontSize = second
Foo: LineSize('14px', '10px')
otherProp: '...'
Foo:
height: '14px'
lineHeight: '14px'
fontSize: '10px'
otherProp: '...'
# TextEllip$ make ellipsis text easy
Foo: TextEllip$
otherProp: '...'
Foo:
whiteSpace = 'nowrap'
overflow = 'hidden'
textOverflow = 'ellipsis'
otherProp: '...'
# ClearFix$, well... just the classic ClearFix
Foo: ClearFix$
otherProp: '...'
Foo:
'*zoom': 1
$before_$after:
content: "''"
display: 'table'
$after:
clear: 'both'
otherProp: '...'
There're two very special Mixins that can be used as example show mss turn @
rules into composable functions:
mss
mss.MediaQuery(
all:
maxWidth: '1200px'
_handheld:
minWidth: '700px'
$tv:
color: '#red'
)(
Content:
width: '960px'
)
css
@media all and (max-width:1200px),not handheld and (min-width:700px),only tv and (color:#red){
.Content{
width:960px;
}
}
The rules for MediaQuery
is: use _
for not
and $
for only
, that's all. We got a similar KeyFrames
too:
mss
mss.KeyFrames('myKeyFrameAnima')
10:
top: '20px'
20:
top: '40px'
30:
top: '50px'
css
@keyframes myKeyFrameAnima{
33.333333333333336%{
top:20px;
}
66.66666666666667%{
top:40px;
}
100%{
top:50px;
}
}
The keys are numbers that will be normalized to pencentage, so you can just write them in proportion.
Note there's one thing to be taken care of, @rules
don't have local namespace, it's a design of css not mss, so make sure the namespace is not used, eg. animation names, selectors inside media query are unique in the same mss tree.
You can define other functions to achieve more interesting effect easily based on mss's nested object presentation, here's a simple built-in function TRAVERSE
, it's UPPER_CASE
to distinguish from functions and mixins, it can be used in some interesting situations such as substitute all static assets' url.
originMss =
Foo:
p:
otherProp: '...'
Bar:
otherProp: '...'
span:
background: url('debug.png')
mssFn = (selector, mss) ->
if selector == 'Bar'
mss.padding = '2px'
mss
propFn = (propName, propValue) ->
if propName == 'background'
propValue.replace(/^url\(.+\)$/g, 'product.png')
else propValue
TRAVERSE(originMss, mssFn, propFn) ==
Foo:
p:
otherProp: '...'
Bar:
padding: '2px'
otherProp: '...'
span:
background: url('product.png')
Mss provide some basic functions to parse an mss object, insert them to DOM, update, or remove from DOM:
s = mss
myStyle =
body:
width: '640px'
# parse a mss object into a string.
console.log(s.parse(myStyle))
# parse a mss object into a string with prettify.
console.log(s.parse(myStyle, true))
# insert the style to <head>, return the <style> tag's node.
styleTag = s.tag(myStyle)
# insert the style to <head> with id=myStyle, return tag's node.
styleTag = s.tag(myStyle, 'myStyle')
# update the styleTag node's style with new mss object
s.retag(myStyle2, styleTag)
# remove the styleTag node
s.unTag(styleTag)
Use mss with some declarative ui frameworks such as React mithril or mecury to keep web component modular and composable, the basic idea is that a component should manage its own DOM and CSS, and since mss is just plain object or array of objects, it's trivial to nest small component into larger one.
An easy way to do this is providing a function for mss along aside your DOM templete, and call children's mss function inside the parent one just like how you compose the DOM, take following Dialog
written using React as an example:
var react = require('React');
var Dialog = react.createClass({
initState: ...
, render: function(){
return (
<div className="Dialog">...</div>
)
}
});
Dialog.mss = function(bgColor){
return {
Dialog:{
background: bgColor
}
}
};
You can easily embed it into another BiggerDialog
:
var BiggerDialog = react.createClass({
initState:...
, render: function(){
return (
<div className="BiggerDialog">
<Dialog ... />
...
</div>
)
}
});
BiggerDialog.mss = function(bgColor, childDialogBgColor){
return {
BiggerDialog: m.merge([
{
background: bgColor
, ...
}
, ...
, Dialog.mss(childDialogBgColor)
])
}
};
At the top level, insert style into DOM using tag
, remember to insert style before inserting any template, so the screen won't flash:
var s = require('mss.js');
s.tag(BiggerDialog.mss('red', 'green'));
react.render(BiggerDialog, document.body);
We can do the similar thing with mithril, here's a version with mithril and coffee(which i'm using everyday in my work):
class Dialog
constructor: (...) ->
view: ->
m '.Dialog', '...'
Dialog.mss = (bgColor) ->
Dialog:
background: color
class BiggerDialog
constructor:
@childDialog = new Dialog(...)
view: ->
m '.BiggerDialog', mss.merge [
childDialog.view()
,
m '.OtherThings', '...'
,
...
]
BiggerDialog.mss = (bgColor, childDialogBgColor) ->
BiggerDialog: [
background: color
OtherThings:
...
,
Dialog.mss(childDialogBgColor)
,
...
]
BTW. The online compiler's page's style is powered by mss(written in coffeescript), here is the source code for amusing:
s = mss
html_body: s.Size('100%', '100%')
overflow: 'hidden'
fontSize: '14px'
$I: s.PosRel(0, 0) s.Size('100%', '100%')
background: '#eee'
Doc: s.PosAbs(0, '50%', 0 , 0) s.Size('46%', '100%')
padding: '2%'
overflow: 'scroll'
LiveParser: s.PosAbs(0, 0, 0, '50%') s.Size('46%', '96%')
padding: '2%'
MssInput_MssOutput:
border: '1px solid #ccc'
background: '#fff'
fontSize: '1em'
MssInput: s.Size('100%', '44%')
background: '#F5F2F0'
ParseHint: s.Size('100%', '2%')
padding: '2%'
MssOutput: s.Size('100%', '44%')
background: '#F5F2F0'
The compiled CSS is directly inserted into DOM:
html,
body {
overflow: hidden;
font-size: 14px;
width: 100%;
height: 100%;
}
#i {
background: #eee;
width: 100%;
height: 100%;
position: relative;
top: 0;
right: 0;
}
#i .Doc {
padding: 2%;
overflow: scroll;
width: 46%;
height: 100%;
position: absolute;
top: 0;
right: 50%;
bottom: 0;
left: 0;
}
#i .LiveParser {
padding: 2%;
width: 46%;
height: 96%;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 50%;
}
#i .LiveParser .MssInput,
#i .LiveParser .MssOutput {
border: 1px solid #ccc;
background: #fff;
font-size: 1em;
}
#i .LiveParser .MssInput {
background: #F5F2F0;
width: 100%;
height: 44%;
}
#i .LiveParser .ParseHint {
padding: 2%;
width: 100%;
height: 2%;
}
#i .LiveParser .MssOutput {
background: #F5F2F0;
width: 100%;
height: 44%;
}
If you don't mind too much verbose here's javascript version:
var s = mss;
html_body: s.Size('100%', '100%')({
overflow: 'hidden',
fontSize: '14px'
}),
$I: s.PosRel(0, 0)(s.Size('100%', '100%')({
background: '#eee',
Doc: s.PosAbs(0, '50%', 0, 0)(s.Size('46%', '100%')({
padding: '2%',
overflow: 'scroll'
})),
LiveParser: s.PosAbs(0, 0, 0, '50%')(s.Size('46%', '96%')({
padding: '2%',
MssInput_MssOutput: {
border: '1px solid #ccc',
background: '#fff',
fontSize: '1em'
},
MssInput: s.Size('100%', '44%')({
background: '#F5F2F0'
}),
ParseHint: s.Size('100%', '2%')({
padding: '2%'
}),
MssOutput: s.Size('100%', '44%')({
background: '#F5F2F0'
})
}))
}))
That'all, happy hacking!
v0.3.0
change array behavior, now please use mss.merge
to merge multiple objects.