Control the responsive style of html elements without creating one-off classes and media queries in your stylesheets. Here's a small demo.
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
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.
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 |
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
.
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) {
/* .... */
}
Neither is currently implemented but may be at a later time.
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 |
When using breakpoint sets, two additional data-attributes control which selector contains breakpoint set CSS variable and the name of the CSS variable:
The breakpoint set variable for the element will be picked off this selector.
Controls the name of the CSS variable from which the breakpoint set is parsed:
html {
/* ^ the selector */
--breakpoints-default: "json...";
/* the key ^^^^^^^ */
}
The src/scss
folder contains example code for bulma, bootstrap and foundation to render each framework's breakpoint-map into your stylesheet.
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.
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 |
The RespStyleAttr
Object provides the main class Css
, and the helper functions init
, refresh
and get
.
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.
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.
RespStyleAttr.get()
will yield a map of all instances. Pass an instance key to get only that instance.
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.
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
})
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 }
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 |
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 }
// }