Unleash JavaScript to create custom styling for every user of a web application. PowerCSS employs merging, caching, compression, and double-buffering to exceed the flexibility - and often the speed - of static CSS files. Here is a presentation of some of the PowerCSS concepts in practice. The dedicated website is at powercss.org.
A significant problem with traditional CSS files - whether written by an expert or someone using {less} or Sass - is that they are not written at run-time. With static CSS, application controlled styling that is responsive to the user's environment is either limited or simply not possible.
PowerCSS provides the tools to write and apply infinitely adjustable CSS based on almost any real-time data available to an application: device orientation, ambient temperature, ambient light, GPS location, heart rate, or time of day. Traditional static CSS files can't compete with this flexibility.
We feel that PowerCSS has achieved its primary goal and is often better than static CSS in many other respects as well. It provides a simple and familiar API where experienced CSS authors can use their existing skills to be up and running in minutes. A minified PowerCSS solution can download faster, render faster after loading, and can speed up some operations by 10x or more compared to traditional CSS. What's not to like?
- MIT license
- No dependencies
- Real-time styling - Create custom styling for every user at any time.
- Pure JS - Remove the need for any static CSS files.
- Name-spaced - Play well with frameworks, jQuery, other libraries, and third-party JavaScript.
- Double-buffering - Minimize page re-flows with this automatic feature. It can speed up some styling changes by more than 10x.
- Merging and caching - Control when styling is updated using time-based minimal processing.
- Mixins - Create custom symbols at multiple levels: virtual stylesheet, virtual cascade, and global. Change a mixin map and watch the styles change immediately.
- Familiar work-flow - Leverage experience with static CSS files using virtual stylesheets and cascades.
- Machine optimized CSS - Have the browser work more efficiently as only one stylesheet is used for styling at any given time, and numerous redundancies are removed during its preparation.
- Highly compressible - Compress styling to a fraction of static stylesheets.
- Quality code - Use well tested and documented code. A commit hook is used to ensure no changes occur unless they pass JSLint and regression tests.
- Media queries and arbitrary-depth conditionals
PowerCSS is a library written in the code style presented in the book Single Page Web Applications - JavaScript end-to-end which is available from Amazon and directly from Manning. It uses a git hook to block any code that fails to pass JSLint and regression tests. All object keys have an underscore prefix and suffix like _this_
which makes them easy targets for compression.
PowerCSS employs a strict data integrity policy: All regular methods never change an argument. Only the utility method _extendRuleMap_
changes an argument and this is explicit stated in the API docs. Conversely, PowerCSS does not return pointers to its arrays or objects; instead one can acquire snapshots using _getAssetJson_
and _getAssetIdList_
.
We were careful to change as little of the existing CSS work-flow as possible. Here are the steps we will take to create our first example:
- Create an HTML document
- Start a JavaScript file
- Add (virtual) stylesheets
- Add and use a (virtual) cascade
Let's get started!
Let's create an HTML file named pcss._ex001_.html
to illustrate the basic capabilities of PowerCSS. A complete copy of this file can be found in the node_modules/powercss/examples
directory or online.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<!-- Mobile settings
see http://www.html5rocks.com/en/mobile/touch.html -->
<meta name="viewport" content="width=device-width user-scalable=no
initial-scale=.8,maximum-scale=.8"/>
<meta name="apple-mobile-web-app-capable" content="yes">
<!-- ie9+ rendering support for latest standards -->
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>PowerCSS Example 001</title>
<script src="../dist/pcss.js"></script>
<script src="js/pcss._ex001_.js"></script>
<script>
function onLoadWin() {
pcss._ex001_();
// ... analytics code ...
}
window.onload = onLoadWin;
</script>
</head>
<body>
<div id="pcss-_head_"></div>
<a href="../"><div class="pcss-_logo_" title="PowerCSS"></div></a>
<h2>Example 001: The basics</h2>
<div class="pcss-_box_">PowerCSS 01<br/>
<input title="name" type="text" placeholder="Name here"/>
</div>
<div class="pcss-_box_">PowerCSS 03</div>
<div class="pcss-_box_">PowerCSS 04</div>
<div class="pcss-_box_">PowerCSS 04</div>
<div class="pcss-_box_">PowerCSS 05</div>
<div class="pcss-_box_">PowerCSS 06</div>
<div class="pcss-_box_">PowerCSS 07</div>
<div class="pcss-_box_">PowerCSS 08</div>
<div class="pcss-_box_">PowerCSS 09</div>
<div class="pcss-_box_">PowerCSS 10</div>
<div class="pcss-_box_">PowerCSS 11</div>
<div class="pcss-_box_">PowerCSS 12</div>
<div class="pcss-_box_">PowerCSS 13</div>
<div class="pcss-_box_">PowerCSS 14</div>
<div class="pcss-_box_">PowerCSS 15</div>
<div class="pcss-_box_">PowerCSS 16</div>
<div class="pcss-_box_">PowerCSS 17</div>
<div class="pcss-_box_">PowerCSS 18</div>
<div class="pcss-_box_">PowerCSS 19</div>
<div class="pcss-_box_">PowerCSS 20</div>
</body>
</html>
Our strategy is to first render the CSS, then the HTML, and then enable analytics. On load, the body
is not displayed. This is changed once the PowerCSS stylesheet is written and enabled.
Let's start a JavaScript file named to pcss._ex001_.js
to provide PowerCSS directives. A complete copy can of this file can be found in the node_modules/powercss/examples/js
directory or online.
We start our module with identification, JSLint settings, and a reminder of preferred CSS attribute order. Then we declare our function variables, and finally we initialize the PowerCSS module.
/* pss._ex001_.js
* Example 001 of run-time generated and managed CSS
* using PowerCSS - the basics
* Michael S. Mikowski - [email protected]
*/
/*jslint browser : true, continue : true,
devel : true, indent : 2, maxerr : 50,
newcap : true, nomen : true, plusplus : true,
regexp : true, sloppy : true, vars : false,
white : true, todo : true, unparam : true
*/
/*global pcss */
/* I. Recommended units: rem and %.
* II. Recommended order: Outside-In
* 1. display, visibility, opacity, z-index
* 2. box-sizing, position, floats, clear
* 3. top, right, bottom, left and vertical-align
* 4. margin defs, box-shadow
* 5. border, border-radius
* 6. height, width
* 7. padding, overflow, cursor
* 8. background, text-align, white-space,
* 9. content defs - font-size, line-height, font, color everything else
* -- break --
* 10. transition or animation definitions
*/
// BEGIN pcss._ex001_
pcss._ex001_ = function () {
var baseSelectorList, boxSelectorList;
pcss._initModule_();
Yes, Virginia, our code really does pass JSLint.
Virtual stylesheets (vsheets) contain the same information as a traditional CSS file but in JSON format and using symbols instead of literal strings. Let's add two vsheets definition to pcss._ex001_.js
. Don't worry about the syntax yet; we will discuss that in the Mixin maps section.
// Begin define selector lists
baseSelectorList = [
{ _selector_str_ : 'body',
_rule_map_ : {
_display_ : '_block_',
_margin_ : '_0_',
_padding_ : [[ '_2d5rem_', '_2rem_' ]],
_background_ : '_xddd_',
_font_family_: '_font_sans_',
_font_size_ : [ '16px' ],
_color_ : '_x888_'
}
},
{ _selector_str_ : 'input',
_rule_map_ : {
_margin_ : '_d5rem_',
_width_ : [ '10rem' ],
_border_ : [[ '_d125rem_', '_solid_', '_xddd_' ]],
_border_radius_ : '_d5rem_',
_outline_ : '_none_',
_padding_ : '_d5rem_',
_background_ : '_x888_',
_font_size_ : '_1rem_',
_color_ : '_xddd_'
}
},
{ _selector_str_ : 'input:focus',
_rule_map_ : {
_border_color_ : '_xfff_',
_background_ : '_x444_',
_color_ : '_xfff_'
}
},
{ _selector_str_ : '.pcss-_logo_',
_rule_map_ : {
_background_image_ : [
'url(http://mmikowski.github.io/images/2016-02-22-pcss.png)'
],
_background_size_ : '_cover_',
_width_ : [ '20.75rem' ],
_height_ : [ '10.125rem' ]
}
},
{ _selector_str_ : '#pcss-_head_',
_rule_map_ : {
_position_ : '_fixed_',
_z_index_ : '_1_',
_top_ : '_0_',
_left_ : '_0_',
_right_ : '_0_',
_height_ : '_2rem_',
_box_shadow_ : [[
['rgba( 64, 32, 32, .5)'], '_0_', '_0_', '_d5rem_', '_0_'
]],
_padding_ : '_0_',
_background_ : '_xeee_'
}
}
];
boxSelectorList = [
{ _selector_str_ : '.pcss-_box_',
_rule_lock_list_ : [ '_font_size_' ],
_rule_map_ : {
_display_ : '_inline_block_',
_opacity_ : '_1_',
_box_sizing_ : '_border_box_',
_position_ : '_relative_',
_vertical_align_ : '_top_',
_margin_ : '_1rem_',
_box_shadow_ : [[
[ 'rgba( 0, 0, 0, .5)' ], '_0_', '_0_', '_d25rem_', '_0_'
]],
_border_ : [[ '_d25rem_', '_solid_', '_xeee_' ]],
_border_radius_ : '_1rem_',
_width_ : [ '16rem' ],
_height_ : [ '8rem' ],
_padding_top_ : '_1rem_',
_background_ : {
_alt_list_ : [
[ '#f85032' ],
[ '-moz-linear-gradient(left, #f85032 0%, #6d362d 100%)' ],
[ '-webkit-linear-gradient(left, #f85032 0%, #6d362d 100%)' ],
[ 'linear-gradient(to bottom, #f85032 0%, #6d362d 100%)' ]
]
},
_font_size_ : '_1d5rem_',
_font_weight_ : '_800_',
_color_ : '_xfff_',
_text_align_ : '_center_'
}
}
];
// End define selector lists
// Begin Add vsheets
pcss._setVsheet_({
_vsheet_id_ : '_box01_',
_mode_str_ : '_add_',
_selector_list_ : boxSelectorList
});
pcss._setVsheet_({
_vsheet_id_ : '_base_',
_mode_str_ : '_add_',
_selector_list_ : baseSelectorList
});
// End Add vsheets
Selectors are defined in a list because their order is important in CSS. PowerCSS records the vsheet definition, but it doesn't compile it to CSS yet - that comes later. Now that we have two vsheets we can use them in a virtual cascade.
Now we will define a cascade which includes an ordered list of vsheets. This is very much like traditional CSS development where we link to static stylesheet files in an HTML document. A cascade merges multiple vsheets into one. This is similar to how a browser merges multiple CSS files on load. However, with PowerCSS we can have many cascades which are automatically updated whenever any change affects them.
Let's add one now:
// Begin add and use _c01_ cascade
pcss._setCascade_({
_cascade_id_ : '_c01_',
_mode_str_ : '_add_',
_vsheet_id_list_ : [ '_base_', '_box01_' ],
_regen_type_ : '_use_'
});
// End add and use _c01_ cascade
};
We can now save the pcss._ex001_.js
file. Now let's review the results.
When we open pcss._ex001_.html
in a modern browser, we should multiple boxes that have been styled according to the cascade. We can view the generated CSS in the browser using the development tools and modify it as if we had written it ourselves:
/* Begin _base_ style */
body {
display : block;
margin : 0;
padding : 2.5rem 2rem;
background : #ddd;
font-family : arial, helvetica, sans-serif;
font-size : 16px;
color : #888
}
input {
margin : .5rem;
width : 10rem;
border : .125rem solid #ddd;
border-radius : .5rem;
outline : none;
padding : .5rem;
background : #888;
font-size : 1rem;
color : #ddd
}
input:focus {
border-color : #fff;
background : #444;
color : #fff
}
.pcss-_logo_ {
background-image : url(http://mmikowski.github.io/images/2016-02-22-pcss.png);
background-size : cover;
width : 20.75rem;
height : 10.125rem
}
#pcss-_head_ {
position : fixed;
z-index : 1;
top : 0;
left : 0;
right : 0;
height : 2rem;
box-shadow : rgba( 64, 32, 32, .5) 0 0 .5rem 0;
padding : 0;
background : #eee
}
/* End _base_ style */
/* Begin _box_ style */
.pcss-_box_ {
display : inline-block;
opacity : 1;
box-sizing : border-box;
position : relative;
vertical-align : top;
margin : 1rem;
box-shadow : rgba( 0, 0, 0, .5) 0 0 .25rem 0;
border : .25rem solid #eee;
border-radius : 1rem;
width : 16rem;
height : 8rem;
padding-top : 1rem;
background : #f85032;
background : -moz-linear-gradient(left, #f85032 0%, #6d362d 100%);
background : -webkit-linear-gradient(left, #f85032 0%, #6d362d 100%);
background : linear-gradient(to bottom, #f85032 0%, #6d362d 100%);
font-size : 1.5rem;
font-weight : 800;
color : #fff;
text-align : center
}
/* End _box_ style */
Of course, if that was all that PowerCSS provided, why bother? When all we need static styling it is certainly simpler to create traditional CSS files using a nice, comfortable text editor or IDE. However, when we need our application to change styling based on any real-time environmental factor, that's where PowerCSS really shines.
This example is illustrated by pcss._ex002_.html
which can be found in the node_modules/powercss/examples
directory or online. Open the file with a browser to see the results.
Double-buffering is an common technique to minimize processing and flicker across many areas of computer graphics. PowerCSS creates two style
elements, and switches between them to apply CSS. PowerCSS never enables a style
element until the CSS is completely written to it. This allows us to change all styles on a page with just one document re-flow, which can be insanely fast compared to changing styles individually from multiple stylesheets.
PowerCSS is intended to replace all stylesheets for an application. While we can use external sheets for our CSS during development, we shouldn't need them for production release. PowerCSS plays very nicely with others and is designed to avoid conflict with third-party web components.
This example is illustrated by pcss._ex003_.html
which can be found in the node_modules/powercss/examples
directory or online. Open the file with a browser to see the results.
A CSS rule declaration looks like the following:
background-color : #fff;
The expression to the left of the color we refer to as the rule key. The text after the colon but before the semicolon we refer to as the rule value. Here the key is color
and the value is #fff
.
Our vsheets are defined using symbol substitution. That is, we use a symbol to indicate the actual key and often the value instead of the literal values. For example, to declare background-color, we use:
_background_color_ : '_xfff_'
At first glance, that might seem silly. However, using symbols for rule keys and values help us greatly when compressing our files. In the example above, our JavaScript rule can be compressed like so:
nx:'qr'
This is 24% of the size of the native CSS (7 vs 29 characters). And some CSS keys and values can be especially verbose. One downside to this approach, of course, is we must declare the symbols initially. The default pcss._cfg_._cssKeyMap_
and pcss._cfg_._cssValMap_
are provided by the pcss.cfg.js
library. cssKeyMap
provides the key symbols and cssValMap
provide the value symbols.
We have compiled a pretty exhaustive list of commonly used keywords and values but the rule keys needed will vary depending on project. If we use a unknown key during development a warning is logged to the JavaScript console and the rule is skipped. One can easily copy these symbols into pcss.cfg.js
and make sure they are defined in cssKeyMap
or cssValMap
as required.
When we have completed a project we can use a script to report the number of uses for each _symbol_
. A symbol that only appears once is not used except for its declaration, and therefore can be safely pruned.
There are four types of CSS value substitution supported by PowerCSS:
- Mixin values :
'_key_'
- Literals :
[ 'literal' ]
- Alternates :
{ _alt_list_ : [ '_key_', [ 'literal' ], ... ] }
- Concatenated :
[[ '_key_', [ 'literal' ], ... ]]
In addition, we can lock
a rule value in a cascade.
mixin maps are settable at the vsheet, cascade, or global level as shown below:
// Vsheet option - add
pcss._setVsheet_({
_vsheet_id_ : '_base_',
_mode_str_ : '_add_',
_selector_list_ : base_selector_list,
_mixin_map_ : base_mixin_map
});
// Vsheet option - change
pcss._setVsheet_({
_vsheet_id_ : '_base_',
_mode_str_ : '_change_',
_mixin_map_ : base_mixin_map
});
// Cascade option - add
pcss._setCascade_({
_cascade_id_ : '_c01_',
_mode_str_ : '_add_',
_vsheet_id_list_ : [ '_base_', '_box_' ],
_mixin_map_ : c01_mixin_map
});
// Cascade option - change
pcss._setCascade_({
_cascade_id_ : '_c01_',
_mode_str_ : '_change_',
_mixin_map_ : c01_mixin_map
});
// Global option
pcss._setGlobalMixinMap_({
_change_type_ : '_replace_',
_mixin_map_ : global_mixin_map,
});
A mixin map is simple key-value pair object as shown below:
// Example mixin map
mixin_map = {
_body_font_size_ : '16px',
_body_font_color_ : '#a44',
_input_width_ : '10rem',
_input_border_ : '.125rem solid #ddd'
};
We can get a copy of a mixin map using _getAssetJson_
.
PowerCSS uses values from four mixin map types:
- The built-in value map,
cssValMap
. This is a set of common CSS values that are available by default. For example, the symbol_fixed_
resolves to 'fixed' in the resulting CSS. The defaultpcss._cfg_._cssValMap_
is provided bypcss.cfg.js
. - The global mixin map is used across all cascades and, as a consequence, by all vsheets they use.
- Cascade mixin maps are exclusive to one cascade and are used by all vsheets in their cascade list.
- Vsheet mixin maps are exclusive to one vsheet.
The precedence of these mixin maps (also known as a 'scope chain') is as follows:
vsheet > cascade > global > built-in
This means that vsheets mixin values have priority over cascade mixin values which have priority over global mixin values which have priority over built-in values. Think of this as "the last match wins." Consider the following PowerCSS rule definition:
rule_map : { _background_ : '_bcolor_', ... }
Now let's define mixin map values at three levels. In pseudo code, it looks something like this:
builtin._bcolor_ = undefined;
global._bcolor_ = 'red';
cascade._bcolor_ = 'green';
vsheet._bcolor_ = 'blue';
Here the vsheet level value, 'blue', "wins" and the CSS processor will use that instead of any cascade, global, or built-in value. In other words, the resulting CSS will read background:blue
.
What if we have multiple vsheets that set _bcolor_
? Easy: the last vsheet in the cascade to set _bcolor_
wins unless the value has been locked earlier in the cascade - see the Locked values section below.
What if we used a vsheet that didn't have a mixin map? Then the mixin value would be defined at just two levels:
builtin._bcolor_ = undefined;
global._bcolor_ = 'red';
cascade._bcolor_ = 'green';
vsheet._bcolor_ = undefined;
Here the cascade level value will "win" and the CSS generator will use 'green' instead of any global or built-in value. And so on. If the value is still undefined at the end of the scope chain, a warning is issued and the property (background
, in this case) is skipped.
An astute reader will notice that a vsheet can be used across many cascades which is a powerful capability. However, do keep this in in mind when setting mixin maps at the vsheet level.
Literal values are just that: a string we want to use as-is. Simply wrap any string in an array to have it read as a literal, as shown below:
rule_map : { _background_ : [ 'blue' ], ... }
We use an array wrapper to identify literals instead of symbol names. This makes our code very compressor friendly.
Sometimes we want to provide alternate rules for a style so that our code will work across multiple browsers. In this case, we can wrap all alternate values in an object with an _alt_list_
property.
{ _selector_str_ : 'body',
rule_map : {
_background_ : {
_alt_list_ : [
'_xfff_',
[ '#f85032' ],
[ '-moz-linear-gradient(left, #f85032 0%, #6d362d 100%)' ],
[ '-webkit-linear-gradient(left, #f85032 0%, #6d362d 100%)' ],
[ 'linear-gradient(to bottom, #f85032 0%, #6d362d 100%)' ]
]
}
}
}
The resulting CSS:
background : #fff;
background : #f85032;
background : -moz-linear-gradient(left, #f85032 0%, #6d362d 100%);
background : -webkit-linear-gradient(left, #f85032 0%, #6d362d 100%);
background : linear-gradient(to bottom, #f85032 0%, #6d362d 100%);
We are not limited literal values as the us of the built-in mixin key like _xfff_
shows. We could even define the entire alternatives map as a mixin, like so:
mixin_map = {
_global_red_grad_map_ : {
_alt_list_ : [
'_xfff_',
[ '#f85032' ],
[ '-moz-linear-gradient(left, #f85032 0%, #6d362d 100%)' ],
[ '-webkit-linear-gradient(left, #f85032 0%, #6d362d 100%)' ],
[ 'linear-gradient(to bottom, #f85032 0%, #6d362d 100%)' ]
]
}
};
... and replace the above declaration in the vsheet definition. If we use gradient many times, we save lots of space, especially after compression. Because now everywhere we want to use it, the line declaration becomes:
_background_ : '_global_red_grad_map_'
This compresses to something like the following, which is 2.9% the size of the minimized CSS equivalent:
ck:'_r'
Remember the order of alternatives in CSS is important: the last supported declaration will always be used.
Sometimes we want to use multiple literal or key values as a single string, usually separated by a space. For this we use a "double list" technique:
{ _selector_str_ : 'li',
_rule_map_ : {
_border_ : [[ '_d125rem_', '_solid_' [ '#f85032' ] ]],
...
}
}
This is very similar to alternate values. The resulting CSS should look like this.
border : .125rem solid #f85032
We can use built-in, mixin and literal values, as the example above shows.
Typically in a cascade, the last property value in "wins". However, it is feasible to prevent overwriting critical properties later in the cascade. One just needs to specify rules to be locked for the provided rule map in the vsheet definition:
box_selector_list = [
{ _select_str_ : '.pcss-_box_',
_locked_rule_list_ : [ '_font_size_' ],
_rule_map_ : {
_font_size_ : [ '16px' ]
// ...
}
}
];
This prevents any later vsheet from overriding the value for
_font_size_
for the .pcss-_box_
selector in the cascade.
An astute reader will again notice that a vsheet can be used across many cascades. Please keep this in mind when locking rule values.
This example is may be found at node_modules/powercss/examples/pcss._ex004_.html
directory or online. Open the file with a browser to see the results.
CSS uses long keywords and values, and is often quite repetitive. A PowerCSS solution can often be compressed to a fraction of the size of minimized CSS. Consider the example above where we created the _global_red_grad_map_
symbol. The initial definition required 276 characters, or 113% of minimized CSS. However, if we use this symbol more than once, we come out far ahead, as the compressed PowerCSS representation is only 2.9% the size of the minimized CSS. If we use the gradient 3 times, the PowerCSS representation is 39.5% the size of minimized CSS. If we use it 5 times, the PowerCSS representation is less than 25% the size.
PowerCSS code and modules that use it can be highly compressed thanks to the use of easily recognized symbols that start and end in an underscore like _css_str_
. After concatenating all JavaScript files in our project, we can replace these symbols with unique tokens, using the shortest tokens for the most common symbols. We call this the SuperPack technique, and we can often reduce file size to 50% of UglifyJS
alone.
In Example 004, the PowerCSS library and the CSS directives were reduced to 30% of their original size, and to 15% when using the SuperPack" technique described above.
This example may be found at node_modules/powercss/examples/pcss._ex005_.html
or online. Open the file with a browser to see the results.
We have taken great care to ensure PowerCSS is as fast, or sometimes even faster than static CSS. We calculate the cascades in software and only provide to the browser a single optimized stylesheet to render. The browser rendering engine doesn't need to work loading and merging sometimes dozens of external stylesheets.
Any change to PowerCSS has up to four process phases:
- Update the data stored in PowerCSS
- Merge the selector list and mixin maps of any affected cascades
- Create and store the CSS of any affected cascades
- Write the CSS to the stylesheet element
The _setVsheet_
, _setCascade_
, and _setGlobalMixinMap_
methods all accept a _regen_type_
parameter which defines how far to proceed with processing on all cascades affected by a change. The accepted values are listed below, along with the defaults:
_regen_type_ value means for the affect cascades:
'_none_' | no processing now
'_merge_' [1] | Merge selector lists and mixin maps now
'_prepare_' | Merge and create CSS now
'_all_' [2] | Merge, create CSS, and if the cascade is currently
| active, double-buffer switch to the new CSS.
'_use_' [3] | Double-buffer switch to new CSS of this cascade
[1] _merge_ This is the default when adding vsheets and cascades
[2] _all_ This is the default when changing vsheets and cascades
It is also the default for _setGlobalMixinMap_
[3] _use_ This is only available for _setCascade_
We recommend sticking with the defaults until and if we see performance issues. Depending on the application, setting _regen_type_
to _all_
for all vsheets and cascades may make the most sense. For others, setting regen_type
to _none_
and then directing all processing steps at their convenience will provide the best solution. Here is an example:
// Initial definition of cascade
pcss._setCascade_({
_cascade_id_ : '_c01_',
_mode_str_ : '_add_',
_vsheet_id_list_ : [ '_base_', '_box_' ],
_mixin_map_ : c01_mixin_map,
_regen_type_ : '_none_'
});
// Vsheet _selector_list_ change
pcss._setVsheet_({
_vsheet_id_ : '_base_',
_mode_str_ : '_change_',
_selector_list_ : base_selector_list,
_regen_type_ : '_none_'
});
// Vsheet _mixin_map_ change
base_mixin_map._red01_ = '#822';
pcss._setVsheet_({
_vsheet_id_ : '_base_',
_mode_str_ : '_change_',
_mixin_map_ : base_mixin_map,
_regen_type_ : '_none_'
});
// Cascade _mixin_map_ change
c01_mixin_map._box_position_ = 'fixed';
pcss._setCascade_({
_cascade_id_ : '_c01_',
_mode_str_ : '_change_'
_mixin_map_ : c01_mixin_map
_regen_type_ : '_none_'
});
// Process step 1: perform merge
pcss._setCascade_({
_cascade_id_ : '_c01_',
_mode_str_ : '_change_'
_regen_type_ : '_merge_'
});
// Process step 2: create CSS
pcss._setCascade_({
_cascade_id_ : '_c01_',
_mode_str_ : '_change_'
_regen_type_ : '_prepare_'
});
// Process step 3: write CSS to style el and enable
pcss._setCascade_({
_cascade_id_ : '_c01_',
_mode_str_ : '_change_'
_regen_type_ : '_use_'
});
The default _regen_type_
may change if test results warrant it.
PowerCSS supports arbitrary-depth conditional expressions
which can be used to construct @media
queries as described in the The Power Cookbook section.
PowerCSS provides the _begin_cond_str_
and _end_cond_str_
objects to create a conditional closure:
selector_list = [
{ _begin_cond_str_ : '.foo' },
{ _selector_str_ : '.baz',
_rule_map_ : { _margin_ : '_0_' }
},
{ _end_cond_str_ : '' },
...
]
This will result in the following invalid CSS text:
.foo{ .baz{ margin : 0 } }
We can nest conditional expressions as deep as we want and PowerCSS will create the closures:
selector_list = [
{ _begin_cond_str_ : '.foo' },
{ _begin_cond_str_ : '.bar' },
{ _begin_cond_str_ : '.bing' },
{ _selector_str_ : '.baz',
_rule_map_ : { _margin_ : '_0_' }
},
{ _end_cond_str_ : '' },
{ _end_cond_str_ : '' },
{ _end_cond_str_ : '' },
...
]
This will result in the following invalid CSS:
.foo{ .bar{ .bing{ .baz{ margin : 0 } } } }
We can optionally include the end condition string to ensure our closures match. If they do not, a warning is printed to the console:
selector_list = [
{ _begin_cond_str_ : '.foo' },
{ _begin_cond_str_ : '.bar' },
{ _begin_cond_str_ : '.bing' },
{ _selector_str_ : '.baz',
_rule_map_ : { '_margin_' : '_0_' }
}
{ _end_cond_str_ : '.bing' }
{ _end_cond_str_ : '.bar' }
{ _end_cond_str_ : '.foo' }
PowerCSS will combine and remove redundancies for each unique scope. The latest declaration always 'wins'. Consider the following declaration:
selector_list = [
{ _begin_cond_str_ : '.foo' },
{ _selector_str_ : '.baz',
_rule_map_ : {
_top_ : '_0_',
_margin_ : '_0_',
_padding_ : '_1rem_'
}
},
{ _end_cond_str_ : 'foo' },
...
{ _begin_cond_str_ : '.foo' },
{ _selector_str_ : '.baz',
_rule_map_ : {
_top_ : '_1rem_',
_padding_ : '_0_'
}
},
{ _end_cond_str_ : 'foo' },
...
]
This will result in a single conditional expression in the output CSS:
.foo{
.baz{
top : 1rem; /* 2nd declaration */
margin : 0; /* 1st */
padding : 0; /* 2nd */
}
}
Future implementations of CSS will support arbitrary-depth nesting and PowerCSS should be ready with minor changes.
No matter how clean an API, sometimes its easier to think in terms of "what do we want to accomplish." This is perhaps the reason programming language "cookbooks" have been so successful over the years. Our most popular recipes are listed below.
pcss._setVsheet_({
_vsheet_id_ : '_base_',
_mode_str_ : '_add_',
_selector_list_ : [...],
_mixin_map_ : {...}
});
pcss._setVsheet_({
_vsheet_id_ : '_base_',
_mode_str_ : '_change_',
_selector_list_ : [...]
});
pcss._setVsheet_({
_vsheet_id_ : '_base_',
_mode_str_ : '_change_',
_mixin_map_ : {...}
});
The following selector_list
adjustments will provide a media query closure:
selector_list = [
{ _begin_cond_str_ : '@media all and (max-width: 550px)' },
{ _selector_str_ : '.my-foot-box',
_rule_map_ : { _margin_ : '_0_' }
}
{ _end_cond_str_ : '' }
]
The resulting CSS:
@media all and (max-width: 550px){
.my-foot-box{ margin : 0 }
}
Using the bullet-proof font declaration format:
selector_list = [
{ _selector_str_ : '@font-face',
_rule_map_ : {
_font_family_ : [ 'myFont' ],
_src_ : [
"url('font/myFont.eot?') format('embedded-opentype'),"
+ "url('font/myFont.woff') format('woff'),"
+ "url('font/myFont.ttf') format('truetype')"
]
}
},
...
]
Deleting a selector list independently is not supported. However, one may change the _selector_list_
to an empty array for a similar effect. Setting the value to undefined
will not work.
pcss._setVsheet_({
_vsheet_id_ : '_base_',
_mode_str_ : '_change_',
_selector_list_ : []
});
Deleting a vsheet mixin map independently is not supported. However, one may change _mixin_map_
to an empty object for a similar effect. Setting the value to undefined
will not work.
pcss._setVsheet_({
_vsheet_id_ : '_base_',
_mode_str_ : '_change_',
_mixin_map_ : {}
});
Recall that deleting a single vsheet will redefine all cascades that use it. Setting _regen_type_
to _none_
will ensure no processing will take place as a result of this call, whereas setting it to _all_
will result in all cascades being processed as fully is possible. If an active cascade is currently in use, it will double-buffer switched to the updated CSS as soon as it is ready.
pcss._setVsheet_({
_vsheet_id_ : '_base_vsheet',
_mode_str_ : '_delete_',
_regen_str_ : '_none_'
});
pcss._getAssetJson_({
_asset_type_ : '_vsheet_'
_asset_id_ : '_base_'
});
pcss._getAssetJson_({
_asset_type_ : '_vsheet_'
_asset_subtype_ : '_selector_list_'
_asset_id_ : '_base_'
});
Because PowerCSS never changes our data, we can create a selector list definition, use it to create a vsheet, modify it, and then use it to create another, vsheet. This process can be repeated indefinitely. Here is an example:
// Define _box01_ vsheet
box_selector_list = [
{ _selector_str_ : '.pcss-_box_',
_rule_lock_list_ : [ '_font_size_' ],
_rule_map_ : {
_display_ : '_inline_block_',
_opacity_ : '_1_'
}
}
];
pcss._setVsheet_({
_vsheet_id_ : '_box01_',
_mode_str_ : '_add_',
_selector_list_ : box_selector_list
});
// Modify and create _box01_ vsheet
pcss._extendRuleMap( box_selector_list, { _color_ : '_x444_' } );
pcss._setVsheet_({
_vsheet_id_ : '_box02_',
_mode_str_ : '_add_',
_selector_list_ : box_selector_list
});
pcss._setCascade_({
_cascade_id_ : '_std_',
_mode_str_ : '_add_',
_vsheet_id_list_ : [ '_base_', '_box_' ]
_mixin_map_ : {...}
});
pcss._setCascade_({
_cascade_id_ : '_std_',
_mode_str_ : '_change_',
_vsheet_id_list_ : [...]
});
pcss._setCascade_({
_cascade_id_ : '_std_',
_mode_str_ : '_change_',
_mixin_map_ : {...}
});
Deleting a vsheet ID list independently is not supported. However, one may change the _vsheet_id_list_
to an empty array for a similar effect. Setting the value to undefined
will not work.
pcss._setCascade_({
_cascade_id_ : '_std_',
_mode_str_ : '_change_',
_vsheet_id_list_ : []
});
Deleting a mixin map independently is not supported. However, one may change the _mixin_map_
to an empty array for a similar effect. Setting the value to undefined
will not work.
pcss._setCascade_({
_cascade_id_ : '_std_',
_mode_str_ : '_change_',
_mixin_map_ : {}
});
Deleting a cascade will only affect that cascade, so setting
_regen_type_
has no affect. If a cascade is in-use,
the styling will be removed.
pcss._setCascade_({
_cascade_id_ : '_std_',
_mode_str_ : '_delete_'
});
pcss._setCascade({
_cascade_id_ : '_std_',
_mode_str_ : '_change_',
_regen_type_ : '_prepare_'
}
pcss._setCascade({
_cascade_id_ : '_std_',
_mode_str_ : '_change_',
_regen_type_ : '_use_'
}
pcss._getAssetJson_({
_asset_type_ : '_std_'
_asset_id_ : '_normal_'
});
pcss._getAssetJson_({
_asset_type_ : '_cascade_'
_asset_subtype_ : '_vsheet_id_list_'
_asset_id_ : '_std_'
});
style_el_prefix = pcss._initModule_({_style_el_prefix_ : 'ns' });
The style element prefix may only be set once on the initial call. Subsequent calls will ignore any request to change this.
style_el_prefix = pcss._initModule_();
Call _initModule_
without arguments to get the current style element prefix.
pcss._togglePcss_( false );
pcss._togglePcss_( true );
pcss._togglePcss();
pcss._setGlobalMixinMap_({
_change_type_ : '_replace_',
_mixin_map_ : { ... }
});
_pcss_merged_
_pcss_prepared_
_pcss_used_
Example | document.addEventListener( '_pcss_prepared_', onPrepared );
Purpose | Report process states for cascades
Attributes | event_obj._data_ is set to the _cascade_id_
Settings | none
Notes | Every time a cascade merge, prepare, or use is enabled, an
| event tied to the document object is fired.
Example | pcss._initModule_({
| _style_el_prefix_ : 'ns',
| _css_key_map_ : {...},
| _css_val_map_ : {...}
| });
Purpose | Initializes style elements using the provided prefix
Arguments | _style_el_prefix_ (opt) A prefix to name-space the two
| <style> elements. If not provided, the prefix 'pcss'
| will be used.
| _css_key_map_, _css_val_map_ (opt) Maps to used to look-up
| CSS keys and values respectively.
Settings | none
Throws | A string error object if style elements already exist
Returns | The style prefix, e.g. 'ns-'
Example | pcss._extendRuleMap_( rule_map, { _color_ : '_x444_' } );
Purpose | A utility to extend a rule_map with new or revised
| values. Providing a value of 'null' deletes a key.
| pcss._extendRuleMap_( rule_map, { _color_ : null } );
Arguments | (positional)
| 0 : base_map - the map to be extended
| 1 : extend_map - the map containing new key-value pairs
Settings | none
Throws | none
Returns | none
| - base_map is modified.
| - extend_map is not.
Example | pcss._setGlobalMixinMap_({
| _change_type_ : '_replace_' or '_merge_'
| _mixin_map_ : mixin_map
| });
Purpose | Replace or merge copy of map provided with globla mixin map
Arguments | _change_type_ (opt) <'_change_'>, '_merge_'
| _change_ - Replace with copy of provided map
| _merge_ - Merge values into existing map
| _mixin_map_ (opt) <{}>
| _regen_type_ (opt) <'_all_'>, '_none_', '_merge_', '_prepare_'
Settings | none
Throws | none
Returns | The number of vsheets affected by the change
Example | pcss._togglePcss_( true );
Purpose | Enable or disable PowerCSS
Arguments | Boolean (optional)
| If not provided, will toggle PowerCSS on or off.
| If provided, true will turn PowerCSS on, and false
| will turn PowerCSS off.
Settings | none
Throws | none
Returns | true (enabled) or false (disabled)
Example | vsheet_id_list = pcss._getAssetIdList_({
| _asset_type_ : '_vsheet_'
| });
| cascade_id_list = pcss._getAssetIdList_({
| _asset_type_ : '_cascade_'
| });
Purpose | Return the list of all vsheets or cascades.
Arguments | _asset_type_ (req), either '_vsheet_' or '_cascade_'
Settings | none
Throws | none
Returns | A list of the asset IDs requested.
Example | pcss._getAssetJson_({
| _asset_id_ : '_c01_',
| _asset_type_ : '_cascade_'
| _asset_subtype_ : '_vsheet_id_list_'
| })
Purpose | Return a JSON snapshot of a vsheet or cascade.
Arguments | _asset_id_ (req) The existing ID of either a cascade
| or a vsheet.
| _asset_type_ (req) '_vsheet_', '_cascade_',
| '_global_mixin_', '_el_cascade_list_',
| _asset_subtype_ (opt)
| '_vsheet_' supports:
| _vsheet_id_, _selector_list_,
| _selector_ms_, _mixin_map_,
| _mixin_ms_
| '_cascade_' supports
| _cascade_id_, _vsheet_id_list_
| _vsheet_ms_, _mixin_map_
| _mixin_ms_, _merged_selector_list_
| _merged_mixin_map_, _merged_selector_ms_
| _css_str_ _css_ms_
Settings | none
Throws | none
Returns | A JSON string of the requested asset.
| If there is no corresponding asset, the JSON string
| returned is 'undefined'.
See The PowerCSS cookbook section to see how _setVsheet_
may be used to accomplish common tasks.
Example | pcss._setVsheet_({
| _vsheet_id_ : '_base_',
| _mode_str_ : '_add_',
| _selector_list_ : base_selector_list,
| _mixin_map_ : {},
| _regen_type_ : '_merge_'
| });
Purpose | Adds, changes, or deletes a vsheet
Arguments | _vsheet_id_ (req) The ID for a vsheet
| _mode_str_ (req) '_add_', '_change_', or '_delete_'
| _selector_list (opt) List of selectors this vsheet will
| represent in PowerCSS format.
| _mixin_map_ (opt) The mixin_map for this vsheet.
| _regen_type_ (opt) '_none_', '_merge_', '_prepare_',
| or '_all_'
Notes | _regen_type_ defaults to '_merge_' on Add, '_all_'
| on other operations.
Settings | none
Throws | none
Returns | vsheet_id, or undef on failure
See The PowerCSS cookbook section to explore how _setCascade_
may be used to accomplish common tasks.
Example | pcss._setCascade_({
| _cascade_id_ : '_c01_',
| _mode_str_ : '_add_',
| _vsheet_id_list_ : [ '_base_', '_box_' ],
| _mixin_map_ : {},
| _regen_type_ : '_none_'
| });
Purpose | Adds, changes, or deletes a cascade
Arguments | _cascade_id_ (req) The ID for a cascade
| _mode_str_ (req) '_add_', '_change_', or '_delete_'
| _vsheet_id_list_ (opt) Ordered list of vsheet ids
| _mixin_map_ (opt) The mixin_map for this cascade.
| _regen_type_ (opt) '_none_', '_merge_', '_prepare_',
| or '_all_' (default is _merge_)
Notes | _regen_type_ defaults to '_merge_' on Add, '_all_'
| on other operations.
Settings | none
Throws | none
Returns | cascade_id, or undef on failure
Example | pcss._getGlobalMixinMap_();
Purpose | Returns the currently set global mixin map.
Arguments | none
Notes | This returns a mixinmap pointer, which can lead to problems
| if the application tampers with the content. Don't do that.
Settings | none
Throws | none
Returns | The global mixin map. This could be undef.
Example | pcss._setStyleAttr_({
| _selector_str_ : '.my_class',
| _attr_key_ : 'font-size',
| _attr_val_ : '12pt'
| });
Purpose | Immediately changes a selector definition in
| the currently active style sheet. In the example provided,
| all text within the selected class would be resized to 12pt.
| Each attribute change can cause a document reflow.
Arguments | _selector_str_ (req) A CSS selector like '#my_id'
| _attr_key_ (req) A CSS attribute like 'color'
| _attr_val_ (req) A CSS value like '#ff0000'
Notes | Sometimes it is more efficient to change a single style than to
| generate and double-buffer-switch a stylesheet. Profile
| code if performance is important!
| Future versions will accept a map of attributes to apply to a
| single selector.
Settings | none
Throws | none
Returns | undef
We may run using the npm
, like so:
cd node_modules/powercss;
npm install; # install development dependencies
npm test; # run regression tests
Code coverage metrics and production deployments are underway to help flush out any remaining refinements or bugs. Assistance with regression tests in the form of code or requested use cases is welcome!
Confirmed to work on Chrome 48, Safari 9, Firefox 44, IE 9+, and Edge browsers. We expect it to work on much earlier versions of Chrome, Safari, and Firefox, but have yet to determine how low we can go.
2016 Michael S. Mikowski (mike[dot]mikowski[at]gmail[dotcom])
MIT
- (x) First "public release" versions of PowerCSS with a working example.
- (x) Added double-buffering support and example.
- (x) Expanded API.
- (x) Added mixin maps at 4 levels.
- (x) Reverted to true double-buffering ( only 2 style elements ).
- (x) Expanded API.
- (x) Documented API reference section.
- (x) Refactored and greatly simplify API.
- (x) Implemented time-based minimal processing.
- (x) Added
_regen_type_
option to manage CSS generation processing. - (x) Added
Recipes
section and expanded API reference. - (x) Added inline API docs
- (x) Restructured project and launched powercss.org.
- (x) Added published events.
- (x) Created multiple automated regression tests.
- (x) Used NodeJS for testing with nodeunit-b.
- (x) Improved error handling.
- (x) Refactored resolver.
- (x) Made events cross-browser compatible (IE9+).
- (x) Added resolver regression tests.
- (x) Released 2016-03-25.
- (x) Released 2016-03-25.
- (x) Added support for CSS conditional expressions.
- (x) Changed built-in keys to use "bottom" instead of "btm", as this was
needlessly confusing. Example:
_border_btm_
becomes_border_bottom_
. - Added method
_getGlobalMixinMap_
. - Fully backward compatible to 1.0 API.
- (x) Released 2016-09-03.
- (x) Moved key and value maps to pcss.cfg.js.
- (x) Adjusted initModule to allow support for custom keys and values.
- (x) Added
_getCssKeyMap_
and_getCssValMap_
methods. - (x) Fully backward compatible to 1.0 API
- (x) Release 2016-09-12.
- (x) Expanded keywords support in pcss.cfg.js.
- (x) Removed unfinished bower support.
- (x) Added
setStyleAttr
which provides capability to change styles after the stylesheet has been written. - (x) Fully backward compatible to 1.0 API.
- (x) Update
setStyleAttr
to accept a map of attributes and values to apply for a selector.
- (x) Release 2017-07-19.
- (x) Convert from nodeunitb to nodeunit + jsdom
- (x) Add coverage reporting to coveralls.io
- (x) Update tests to expand coverage
- (x) Selector string without rule map is inserted verbatim into CSS
- (o) Add extendo-map expansion
- (o) Use a double-buffered stylesheet per cascade. This would better support isolated web feature components, e.g. one for a chat feature, one for a comment feature, etc. Deleting a cascade should remove the associated stylesheet(s). Write the double-buffer stylesheet elements only as needed.
- (o) Provide tools to convert existing CSS to PowerCSS JavaScript.
- (o) Extendo-map expansion
Any improvements or suggestions are welcome.