PowerCSS - JavaScript-powered real-time CSS
Overview
Unleash JavaScript to create custom styling for every person that uses
your 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.
The Goal
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?
Key benefits
- Real-time styling - Create custom styling for every user of your
application 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 your 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 your 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 your styling to a fraction of static CSS.
- 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
- MIT license
- No dependencies
Code Style
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_
.
Example 001: The basics
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!
1. Create an HTML document
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">
<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">
<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_();
}
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="your 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.
2. Start a JavaScript file
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.
pcss._ex001_ = function () {
var baseSelectorList, boxSelectorList;
pcss._initModule_();
Yes, Virginia, our code really does pass JSLint.
3. Add (virtual) stylesheets
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.
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_'
}
}
];
pcss._setVsheet_({
_vsheet_id_ : '_box01_',
_mode_str_ : '_add_',
_selector_list_ : boxSelectorList
});
pcss._setVsheet_({
_vsheet_id_ : '_base_',
_mode_str_ : '_add_',
_selector_list_ : baseSelectorList
});
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, let's use them in a virtual cascade.
4. Add and use 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:
pcss._setCascade_({
_cascade_id_ : '_c01_',
_mode_str_ : '_add_',
_vsheet_id_list_ : [ '_base_', '_box01_' ],
_regen_type_ : '_use_'
});
};
We can now save the pcss._ex001_.js
file. Now let's review the results.
What we have wrought
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:
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
}
.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
}
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.
Example 002: Double-buffering
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 your 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.
Example 003: Mixin maps
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 your browser to see the results.
Symbol substitution
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 cssKeyMap
defines our rule key symbols in the PowerCSS library
file, pcss.js
, and cssValMap
defines our common rule value symbols.
Value symbols may also be extended with mixin maps.
We have compiled a pretty exhaustive list of commonly used keywords and values,
but the rule keys needed will vary depending on project. During development,
if we use a unknown key, a warning is logged to the JavaScript console and
the rule is skipped. One can easily copy these symbols into pcss.js
and make sure they are defined in cssKeyMap
or cssValMap
as required.
Alternately, when we have completed a project, we use a simple Perl script
to report the number of users for each _symbol_
across the project.
A symbol that only appears once is not used except for its declaration,
and therefore can be safely deleted, e.g. "pruned."
We are exploring how to improve the initialization of the cssKeyMap
and cssValMap
data. Suggestions are welcome :)
Rule value substitutions
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.
Setting a mixin map
mixin maps are settable at the vsheet, cascade, or global level
as illustrated below:
pcss._setVsheet_({
_vsheet_id_ : '_base_',
_mode_str_ : '_add_',
_selector_list_ : base_selector_list,
_mixin_map_ : base_mixin_map
});
pcss._setVsheet_({
_vsheet_id_ : '_base_',
_mode_str_ : '_change_',
_mixin_map_ : base_mixin_map
});
pcss._setCascade_({
_cascade_id_ : '_c01_',
_mode_str_ : '_add_',
_vsheet_id_list_ : [ '_base_', '_box_' ],
_mixin_map_ : c01_mixin_map
});
pcss._setCascade_({
_cascade_id_ : '_c01_',
_mode_str_ : '_change_',
_mixin_map_ : c01_mixin_map
});
pcss._setGlobalMixinMap_({
_mode_str_ : '_add_',
_mixin_map_ : global_mixin_map,
});
A mixin map is simple key-value pair object as illustrated below:
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_
.
The four type of mixin maps
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. This map is
found in the the PowerCSS library file, pcss.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.
mixin map precedence
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
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 illustrated below:
rule_map : { _background_ : [ 'blue' ], ... }
We use an array wrapper to identify literals instead of symbol names.
This makes our code very compressor friendly.
Alternate values
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.
Concatenated values
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.
Locked values
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.
Example 004: Compression
This example is illustrated by pcss._ex004_.html
which can
be found in the node_modules/powercss/examples
directory or online.
Open the file with your 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.
Example 005: Performance
This example is illustrated by pcss._ex005_.html
which can
be found in the node_modules/powercss/examples
directory or online.
Open the file with your 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:
pcss._setCascade_({
_cascade_id_ : '_c01_',
_mode_str_ : '_add_',
_vsheet_id_list_ : [ '_base_', '_box_' ],
_mixin_map_ : c01_mixin_map,
_regen_type_ : '_none_'
});
pcss._setVsheet_({
_vsheet_id_ : '_base_',
_mode_str_ : '_change_',
_selector_list_ : base_selector_list,
_regen_type_ : '_none_'
});
base_mixin_map._red01_ = '#822';
pcss._setVsheet_({
_vsheet_id_ : '_base_',
_mode_str_ : '_change_',
_mixin_map_ : base_mixin_map,
_regen_type_ : '_none_'
});
c01_mixin_map._box_position_ = 'fixed';
pcss._setCascade_({
_cascade_id_ : '_c01_',
_mode_str_ : '_change_'
_mixin_map_ : c01_mixin_map
_regen_type_ : '_none_'
});
pcss._setCascade_({
_cascade_id_ : '_c01_',
_mode_str_ : '_change_'
_regen_type_ : '_merge_'
});
pcss._setCascade_({
_cascade_id_ : '_c01_',
_mode_str_ : '_change_'
_regen_type_ : '_prepare_'
});
pcss._setCascade_({
_cascade_id_ : '_c01_',
_mode_str_ : '_change_'
_regen_type_ : '_use_'
});
The default _regen_type_
may change if test results warrant it.
Conditional expressions
Use
PowerCSS supports arbitrary-depth conditional expressions
which
can be used to construct @media
queries as described in the
The Power Cookbook section.
Syntax
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 } }
Nesting
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' }
Redundancies
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;
margin : 0;
padding : 0;
}
}
Future use
Future implementations of CSS will support arbitrary-depth nesting
and PowerCSS should be ready with minor changes.
The PowerCSS cookbook
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.
Virtual stylesheet (vsheet) recipes
Add a vsheet
pcss._setVsheet_({
_vsheet_id_ : '_base_',
_mode_str_ : '_add_',
_selector_list_ : [...],
_mixin_map_ : {...}
});
Change a vsheet selector list
pcss._setVsheet_({
_vsheet_id_ : '_base_',
_mode_str_ : '_change_',
_selector_list_ : [...]
});
Change a vsheet mixin map
pcss._setVsheet_({
_vsheet_id_ : '_base_',
_mode_str_ : '_change_',
_mixin_map_ : {...}
});
Add a media query to a vsheet
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 }
}
Add a font face to a vsheet
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')"
]
}
},
...
]
Delete a vsheet selector list
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_ : []
});
Delete a vsheet mixin map
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_ : {}
});
Delete an entire vsheet
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_'
});
Copy an entire vsheet
pcss._getAssetJson_({
_asset_type_ : '_vsheet_'
_asset_id_ : '_base_'
});
Copy the selector list of a vsheet
pcss._getAssetJson_({
_asset_type_ : '_vsheet_'
_asset_subtype_ : '_selector_list_'
_asset_id_ : '_base_'
});
Use the same selector list to define multiple vsheets
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:
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
});
pcss._extendRuleMap( box_selector_list, { _color_ : '_x444_' } );
pcss._setVsheet_({
_vsheet_id_ : '_box02_',
_mode_str_ : '_add_',
_selector_list_ : box_selector_list
});
Cascade recipes
Add a new cascade
pcss._setCascade_({
_cascade_id_ : '_std_',
_mode_str_ : '_add_',
_vsheet_id_list_ : [ '_base_', '_box_' ]
_mixin_map_ : {...}
});
Change a cascade vsheet id list
pcss._setCascade_({
_cascade_id_ : '_std_',
_mode_str_ : '_change_',
_vsheet_id_list_ : [...]
});
Change a cascade mixin map
pcss._setCascade_({
_cascade_id_ : '_std_',
_mode_str_ : '_change_',
_mixin_map_ : {...}
});
Delete a vsheet id list from a cascade
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_ : []
});
Delete a mixin map from a cascade
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_ : {}
});
Delete an entire cascade
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_'
});
Prepare a cascade so the CSS is ready to use
pcss._setCascade({
_cascade_id_ : '_std_',
_mode_str_ : '_change_',
_regen_type_ : '_prepare_'
}
Use a cascade
pcss._setCascade({
_cascade_id_ : '_std_',
_mode_str_ : '_change_',
_regen_type_ : '_use_'
}
Copy an entire cascade
pcss._getAssetJson_({
_asset_type_ : '_std_'
_asset_id_ : '_normal_'
});
Copy just the vsheet id list of a cascade
pcss._getAssetJson_({
_asset_type_ : '_cascade_'
_asset_subtype_ : '_vsheet_id_list_'
_asset_id_ : '_std_'
});
General recipes
Set the style element prefixes
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.
Get the current style prefix
style_el_prefix = pcss._initModule_();
The _initModule_
method returns the style prefix in use.
Disable PowerCSS
pcss._togglePcss_( false );
Enable PowerCSS
pcss._togglePcss_( true );
Toggle PowerCSS
pcss._togglePcss();
Change the global mixin map
pcss._setGlobalMixinMap_({
_mode_type_ : 'change',
_mixin_map_ : { ... }
});
API reference - Events
_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.
API reference - Methods
_initModule_
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-'
_extendRuleMap_
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.
_setGlobalMixinMap_
Example | pcss._setGlobalMixinMap_({
| _mode_type_ : 'add',
| _mixin_map_ : mixin_map
| });
Purpose | Add, change, delete, or update process status for
| a global mixin map id.
|
Arguments | _mode_type_ (req) '_add_', '_change_', or '_delete_'
| _mixin_map_ (opt)
| _regen_type_ (opt) '_none_', '_merge_', '_prepare_', or '_all_'
Notes | _regen_type_ defaults to '_all_' if not provided.
Settings | none
Throws | none
Returns | The number of vsheets affected by the change
_togglePcss_
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)
_getAssetIdList_
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. PowerCSS will
| NEVER use this list pointer, so you may mutate as
| you please.
_getAssetJson_
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'.
_setVsheet_
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
_setCascade_
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) List of vsheet ids in order of
| application.
| _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
_getCssKeyMap_
(new in 1.2)
_getCssValMap_
(new in 1.2)
_getGlobalMixinMap_
(new in 1.2)
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.
_setStyleAttr_
(new in 1.3)
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 your
| 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
Regression tests
Regression tests are found under the test
directory. You may
run using the npm
, like so:
cd node_modules/powercss;
npm install;
npm test;
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!
Compatibility
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.
Release Notes
Copyright (c)
2016 Michael S. Mikowski (mike[dot]mikowski[at]gmail[dotcom])
License
MIT
Version 0.1.x
- First "public release" versions of PowerCSS with a working example.
Version 0.2.x
- Added double-buffering support and example.
Version 0.3.x
- Made API changes.
- Added mixin support at 4 levels.
- Reverted to true double-buffering ( only 2 style elements ).
Version 0.4.x
- API changes
- API reference section
Version 0.5.x
- Refactored and greatly simplify API.
- Implemented time-based minimal processing.
_regen_type_
option to manage CSS generation processing.- Added
Recipes
section and expanded API reference. - Added In-code API docs
- Restructured project and launched powercss.org.
Version 0.6.x
- Added events.
- Created multiple automated regression tests.
- Used NodeJS for testing with nodeunit-b.
- Improved error handling.
Version 0.7.x
- Refactored resolver.
- Made event cross-browser (IE9+).
- Added resolver regression tests.
Version 1.0.x
Version 1.1.x
- Initial release 2016-03-25.
- Added support for CSS conditional expressions.
- 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.
Version 1.2.x
- Initial release 2016-09-03.
- Moved key and value maps to pcss.cfg.js.
- Adjusted initModule to allow support for custom keys and values.
- Added
_getCssKeyMap_
and _getCssValMap_
methods. - Fully backward compatible to 1.0 API
Version 1.3.x (current)
- Initial release 2016-09-12.
- Expanded keywords support in pcss.cfg.js.
- Removed unfinished bower support.
- Added
setStyleAttr
which provides capability to change styles
after the stylesheet has been written. - Fully backward compatible to 1.0 API.
Version 1.3.x (planned)
- Update
setStyleAttr
to accept a map of attributes and values to
apply for a selector.
Version 1.4.x (planned)
- 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.
- Provide tools to convert existing CSS to PowerCSS JavaScript.
Similar Projects
absurdjs, responsive.j$
Contribute!
If you want to help out with PowerCSS, we are hosted at
GitHub. Any improvements or suggestions are welcome!
You can reach me at mike[dot]mikowski[at]gmail[dotcom].
End