responsive-style-attr v0.0.7
Responsive Style Attributes
Control the responsive style of html elements without creating one-off classes and media queries in your stylesheets. Here's a small demo.
Installation
npm i responsive-style-attr --save
The package contains three frontend builds:
dist/resp-style-attr.cjs.js
- CommonJS bundle, suitable for use in Node.jsdist/resp-style-attr.esm.js
- ES module bundle, suitable for use in other people's libraries and applicationsdist/resp-style-attr.umd(.min).js
- UMD build, suitable for use in any environment (including the browser, as a<script>
tag)
The size of the minified script is ~9kb (~3.5kb gzipped)
There are two builds for headless operation:
dist/resp-style-attr-headless.cjs.js
- CommonJS bundle, suitable for use in Node.jsdist/resp-style-attr-headless.esm.js
- ES module bundle, suitable for use in other people's libraries and applications
Basic Usage
To enable responsive style on an element, just add a data-rsa-style
-attribute containing a JSON object and call RespStyleAttr.init()
:
<h1 data-rsa-style='{"255px-to-512px" : "font-size: 1.5rem", "500px-up" : "font-size:2rem"}'>I change my font size according to screen width</h1>
<script>
window.addEventListener('DOMContentLoaded', function () {
RespStyleAttr.init();
};
</script>
The data-attribute object's keys are expanded to media queries, the rules are put inside selectors that get wrapped by the media queries. The above example would expand to
@media all and (min-width: 255px) and (max-width: 511.98px) {
.rsa-7063658802351566 {
font-size: 1.5rem
}
}
@media all and (min-width: 500px) {
.rsa-8982493736072943 {
font-size: 2rem
}
}
The generated classes are applied to the <h1>
-node. Note that all media queries and style rules are sorted and equalized to avoid duplicate selectors.
Media Query Shorthand Syntax
The media query shorthand syntax lets you combine multiple media query features, separated by an @
-symbol. Examples:
/* "1000px" expands to: */
@media all and (min-width: 1000px) {
}
/* "255px-to-500px@portrait" expands to: */
@media all and (min-width: 255px) and (max-width: 499.98px) and (orientation: portrait) {
}
/* ... see the expansion spec file for more examples */
Why subtract .02px? Browsers don’t currently support range context queries, so we work around the limitations of min- and max- prefixes and viewports with fractional widths (which can occur under certain conditions on high-dpi devices, for instance) by using values with higher precision.
Out of the box, this are supported query shortcuts in the object keys:
name | syntax | description |
---|---|---|
media type | screen | matches one or more given media types (screen,all,print,speech), is expected as first feature in shorthand! |
orientation | portrait | matches given orientation (portrait or landscape ) |
literal up | 800px-up or gt-800px | matches viewports wider than the given value and unit |
literal down | 500px-down or lt-500px | matches viewports narrower than the given value and unit |
literal between | 500px-to-1000px | matches viewports between the two given values |
lte | lte-500px | matches viewports narrower than or equal to given value |
gte | gte-500px | matches viewports wider than or equal to given value |
OR
for different feature sets
You can use @,@
to split a shorthand key into multiple media queries. This is useful when you want to address vendor-specific features for the same style, for example -webkit-min-device-pixel-ratio
and min-resolution
.
Literal Features
To use a media query feature as is, just write it wrapped in parentheses, like this:
/* "lt-1000px@(prefers-color-scheme: dark)" would expand to: */
@media all and (max-width: 999.98px) and (prefers-color-scheme: dark) {
/* .... */
}
Negations and MQL4 Boolean Operators
Neither is currently implemented but may be at a later time.
Using Breakpoint Sets in Shortcuts
In addition to the literal viewport size shortcuts, you can define breakpoint sets in your stylesheet as a CSS variable and use them in shortcuts. For example, a bootstrap 5 breakpoint set CSS variable would look like this:
html {
--breakpoints-default: [["xs","0"], ["sm","576px"], ["md","768px"], ["lg","992px"], ["xl","1200px"], ["xxl","1400px"]];
}
This list is picked up by the breakpoint parser and enables the following shortcuts:
name | syntax | description |
---|---|---|
breakpoint only | md | matches viewports between the given breakpoint and the next larger one (if a larger exists) |
breakpoint up | xs-up or gt-xs | matches viewports wider than the value of the given breakpoint |
breakpoint down | lg-down or lt-xs | matches viewports narrower than the value of the given breakpoint |
breakpoint between | md-to-xl | matches viewports between the two given breakpoints |
mixed between | md-to-1000px , 400px-to-xl | matches viewports between the given breakpoint and the literal value |
lte | lte-500px | matches viewports narrower than or equal to given breakpoint |
gte | gte-500px | matches viewports wider than or equal to given breakpoint |
Controlling Breakpoint Sets
When using breakpoint sets, two additional data-attributes control which selector contains breakpoint set CSS variable and the name of the CSS variable:
data-rsa-selector[="html"]
The breakpoint set variable for the element will be picked off this selector.
data-rsa-key[="default"]
Controls the name of the CSS variable from which the breakpoint set is parsed:
html {
/* ^ the selector */
--breakpoints-default: "json...";
/* the key ^^^^^^^ */
}
Example implementations
The src/scss
folder contains example code for bulma, bootstrap and foundation to render each framework's breakpoint-map into your stylesheet.
Custom Shortcut Features
If you need to go deeper, you can create custom shortcut features that modify every feature of the media query. The custom features must be passed in the options
-object of the init
-function.
let options = {
features: {
androidOnly: function (mediaQuery) {
// this will set the media type to "none" on devices that are not android
if (!/android/i.test(navigator.userAgent)) {
mediaQuery.media = 'none'
}
},
uaMustMatch: function (mediaQuery, input) {
//this will disable the the mediaquery if the useragent does not match input ...
const re = new RegExp(input, 'i');
if (!re.test(navigator.userAgent)) {
mediaQuery.media = 'none';
}
}
}
}
The custom features would be used like this:
<p data-rsa-style='{"androidOnly" : "border: 1px solid #000;"}'>I have a border on android devices</p>
<p data-rsa-style='{"usMustMatch(ios)" : "border: 1px solid #000;"}'>I have a border on iOs devices</p>
If you set a feature to true
it will be written without a value. This is useful for setting features like prefers-reduced-motion
where only the feature key is used in the query. Setting a feature to false
will remove it from the final media query. A feature function can modify, set or remove more than one media query feature.
If you prefix the feature key with :
the key will be omitted from the final media query and only the value will be used. This can be handy for things like level 4 range context where the expression is not in key: value
format.
The full signature of a custom feature function is
/**
* @param mediaQuery object media query map (object) that's currently constructed
* @param inputArgs string|undefined string of the arguments
* @param currentKey string the key that's currently expanded
* @param currentNode HTMLElement|null the node thats currently operated on, if instance runs in DOM context
*/
someFeature(mediaQuery, inputArgs, currentKey, currentNode) {
//...
}
See test/expansion.spec.js
for a few more examples.
Options
Pass options to the RespStyleAttr.init
-function or to the RespStyleAttr.Css
-constructor when creating instances manually.
You can also set options for all instances by modifying the default options via RespStyleAttr.defaultOptions
.
name | type | default | description |
---|---|---|---|
debug | bool | false | controls if verbose information is written to console |
breakpointSelector | string | 'html' | the default breakpoint selector (compare data-rsa-selector ) |
breakpointKey | string | 'default' | the default breakpoint key (compare data-rsa-key ) |
selectorTemplate | function | s => `.rsa-${s}` | a small function that generates the selector used inside the generated stylesheet. Class is used by default but you could also create a data-attribute. Don't create ids because the same selector may be used for multiple elements. |
selectorPropertyAttacher | function | (node, hash) => node.classList.add(`rsa-${hash}`) | a function that actually attaches the property to the node. |
attachStyleNodeTo | string|HtmlElement | 'head' | Selector or node to which the generated style node is attached |
scopedStyleNode | bool | true | controls whether the style node has a scoped attribute |
breakpoints | Array|null | null | Alternative way of passing a breakpoint set to an instances (see "Breakpoint sets" for more information) |
ignoreDOM | bool | false | instructs the instance to ignore the dom, only used for testing |
alwaysPrependMediatype | bool | true | controls if the media type is always set on generated media queries |
minMaxSubtract | float | 0.02 | value that is subtracted from values in certain situations (see notice above) |
useMQL4RangeContext | bool | false | if enabled, screen width query features will be generated in new syntax |
API
The RespStyleAttr
Object provides the main class Css
, and the helper functions init
, refresh
and get
.
init
RespStyleAttr.init()
will pick up all elements in your document that have a data-rsa-style
-attribute and deploy the media queries and styles rules extracted from those attributes. Instances are created for each combination of key
and selector
attributes that are found on the nodes (or implied by default values). The default instance's key would be default_html
.
If you pass options, they will be passed on to every instance created.
refresh
If you add more elements that use responsive style attributes to the document, you can call the RespStyleAttr.init()
-method to process all new and unprocessed elements and deploy their styles.
get
RespStyleAttr.get()
will yield a map of all instances. Pass an instance key to get only that instance.
Manually Creating Instances
Just call let myRSAInstance = new RespStyleAttr.Css()
to create a new instance. You should pass an options-object containing at least the breakpointSelector
and breakpointKey
properties.
If the constructor detects that an instance with the same instance key (consisting of given breakpoint key and breakpoint selector) already exists in the internal instance map, that instance will be refreshed and returned. You also can call refresh
on an existing instance.
Events
There is currently only one event supported: rsa:cssdeployed
will be dispatched on the <style>
-node that belongs to the instance. You can use it like this:
window.addEventListener('rsa:cssdeployed', e => {
console.log(e.detail);
//is a reference to `Css`-instance that dispatched the event
})
Preventing FOUC
If you want to prevent FOUC, add the class rsa-pending
to your elements. When the stylesheet is deployed, the class is removed from each elements' class list. Since the nodes usually aren't measured etc during style creation, just use:
.rsa-pending{ display: none }
/* or */
.rsa-pending{ visibility: hidden }
Headless
The "headless" variant lets you generate stylesheets off of document fragments. It does not rely on the DOM so you can run it in a node environment. Since classList
is not available, data-attributes and data-attribute selectors are generated by default. The headless variant also supports an extra option
name | type | default | description |
---|---|---|---|
removeDataAttribute | bool | false | when true, occurences of data-rsa-style="..." are removed from the given fragment |
Usage
Since Headless
extends Css
, it takes the same options. Parse a fragment like this:
const {Headless} = require('...path-to/resp-style-attr-headless.cjs'),
instance = new Headless(),
someHTML = `<div data-rsa-style='{"lt-400px":"border: 1px solid #000"}'></div>`;
instance.parse(someHTML);
// -> <div data-rsa-style='{"lt-400px":"border: 1px solid #000"}' data-rsa-3523518655946362></div>
// passing true as the second arg will remove the original data-attribute
instance.parse(someHTML, true);
// -> <div data-rsa-3523518655946362></div>
You can call parse
on the same instance repeatedly or pass an entire document. parse
's output will be the input fragment but with selector attributes added.
Adding styles directly is also supported, simply pass an object or json string to the push
method and receive a list of hashes, that you can convert into attributes and attach them yourself:
push
is also supported by the browser variant!
//...
instance.push('{"gt-800px":"background:#f00;"}');
//-> ['data-rsa-6088263273057222']
instance.push({"gt-1000px":"background:#00f;","portrait" : "padding:20px" });
//-> ['data-rsa-366898066636896', 'data-rsa-2976456585877488']
Finally, you can fetch the css that has been generated from all styles by calling getCss
or get a style node by calling getStyleSheet
.
//...
instance.getCss();
// -> @media all and (max-width: 399.98px){
// [data-rsa-3523518655946362]{ border:1px solid #000 }
// }
// @media all and (min-width: 800px){
// [data-rsa-6088263273057222]{ background:#f00 }
// }
// @media all and (min-width: 1000px){
// [data-rsa-366898066636896]{ background:#00f }
// }
// @media all and (orientation: portrait){
// [data-rsa-2976456585877488]{ padding:20px }
// }