├── .gitignore ├── .github └── demo.gif ├── .travis.yml ├── .editorconfig ├── .rollup.js ├── LICENSE.md ├── shorthands.js ├── package.json ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | package-lock.json 4 | index.*.* 5 | .eslintcache -------------------------------------------------------------------------------- /.github/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrenjacoby/postcss-range-value/HEAD/.github/demo.gif -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8 4 | install: 5 | - npm install --ignore-scripts 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = false 10 | -------------------------------------------------------------------------------- /.rollup.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | 3 | export default { 4 | input: 'index.js', 5 | output: [ 6 | { file: 'index.cjs.js', format: 'cjs', sourcemap: true }, 7 | { file: 'index.es.mjs', format: 'es', sourcemap: true } 8 | ], 9 | plugins: [ 10 | babel({ 11 | presets: [ 12 | ['@babel/env', { modules: false, targets: { node: 8 } }] 13 | ] 14 | }) 15 | ] 16 | }; 17 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2020 Darren Jacoby 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /shorthands.js: -------------------------------------------------------------------------------- 1 | export default (prop, values) => { 2 | if (/^(margin|margin-block|margin-inline)$/.test(prop)) { 3 | return margin(prop, values); 4 | } 5 | 6 | if (/^(padding|padding-block|padding-inline)$/.test(prop)) { 7 | return padding(prop, values); 8 | } 9 | 10 | return false; 11 | } 12 | 13 | /** 14 | * Support for margin-block, margin-inline and margin 15 | * 16 | * @param string prop 17 | * @param array values 18 | */ 19 | const margin = (prop, values) => { 20 | const [$1, $2, $3, $4] = values; 21 | 22 | if (['margin-block', 'margin-inline'].includes(prop)) { 23 | return { [`${prop}-start`]: $1, [`${prop}-end`]: $2 || $1 } 24 | } 25 | 26 | return { [`${prop}-top`]: $1, [`${prop}-right`]: $2 || $1, [`${prop}-bottom`]: $3 || $1, [`${prop}-left`]: $4 || $2 || $1 } 27 | }; 28 | 29 | /** 30 | * Support for padding-block, padding-inline and padding 31 | * 32 | * @param string prop 33 | * @param array values 34 | */ 35 | const padding = (prop, values) => { 36 | const [$1, $2, $3, $4] = values; 37 | 38 | if (['padding-block', 'padding-inline'].includes(prop)) { 39 | return { [`${prop}-start`]: $1, [`${prop}-end`]: $2 || $1 } 40 | } 41 | 42 | return { [`${prop}-top`]: $1, [`${prop}-right`]: $2 || $1, [`${prop}-bottom`]: $3 || $1, [`${prop}-left`]: $4 || $2 || $1 } 43 | }; 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-range-value", 3 | "version": "0.3.0", 4 | "description": "PostCSS range values", 5 | "author": "Darren Jacoby", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/soberwp/postcss-range-value.git" 10 | }, 11 | "homepage": "https://github.com/soberwp/postcss-range-value", 12 | "bugs": { 13 | "url": "https://github.com/soberwp/postcss-range-value/issues" 14 | }, 15 | "main": "index.cjs.js", 16 | "module": "index.es.mjs", 17 | "files": [ 18 | "index.cjs.js", 19 | "index.cjs.js.map", 20 | "index.es.mjs", 21 | "index.es.mjs.map" 22 | ], 23 | "scripts": { 24 | "prepublishOnly": "npm test", 25 | "pretest": "rollup -c .rollup.js", 26 | "test": "eslint *.js --cache --ignore-path .gitignore --quiet" 27 | }, 28 | "engines": { 29 | "node": ">= 8.0.0" 30 | }, 31 | "dependencies": { 32 | "postcss": "^7.0.27" 33 | }, 34 | "devDependencies": { 35 | "@babel/core": "^7.9.0", 36 | "@babel/preset-env": "^7.9.5", 37 | "babel-eslint": "^10.0.1", 38 | "eslint": "^5.6.1", 39 | "eslint-config-dev": "^2.0.0", 40 | "rollup": "^2.4.0", 41 | "rollup-plugin-babel": "^4.4.0" 42 | }, 43 | "eslintConfig": { 44 | "extends": "dev", 45 | "parser": "babel-eslint" 46 | }, 47 | "keywords": [ 48 | "postcss", 49 | "css", 50 | "postcss-plugin", 51 | "postcss-range", 52 | "postcss-range-value", 53 | "postcss-responsive" 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # postcss-range-value 2 | 3 | PostCSS plugin to add support for **`range()`**, allowing for a responsive range value between two screen sizes. 4 | 5 | 6 | 7 | ### Installation 8 | 9 | ```shell 10 | yarn add postcss-range-value --dev 11 | ``` 12 | 13 | Require `postcssRange` at the top of Webpack or Mix: 14 | ```js 15 | const postcssRange = require('postcss-range-value'); 16 | ``` 17 | 18 | #### Using Webpack 19 | 20 | ```js 21 | postcss([postcssRange]); 22 | ``` 23 | 24 | #### Using Mix Sass (Sage 10) 25 | 26 | ```js 27 | mix 28 | .sass('resources/assets/styles/editor.scss', 'styles') 29 | .options({ 30 | postCss: [postcssRange] 31 | }); 32 | ``` 33 | 34 | ### Config 35 | 36 | Some config can be passed into `postcssRange()` in Webpack or Mix. 37 | 38 | Example below with the current defaults. 39 | 40 | ```js 41 | postcssRange({ 42 | rootRem: 16, 43 | prefix: 'range', 44 | screenMin: '48rem', // 768 45 | screenMax: '100rem', // 1600 46 | clamp: true, 47 | }) 48 | ``` 49 | 50 | ### Usage 51 | 52 | Range values can be passed to any CSS property that supports sizing. 53 | 54 | `range($min-value, $max-value, $screen-min, $screen-max)` 55 | 56 | The range value will responsively scale from `$min-value` to `$max-value` between `$screen-min` and `$screen-max`. 57 | 58 | #### Basic 59 | 60 | ```scss 61 | .range-value { 62 | font-size: range(2rem, 6rem, 48rem, 87.5rem); 63 | } 64 | ``` 65 | 66 | You can omit `$screen-min` and `$screen-max` if passed in your `postcssRange` config. 67 | 68 | ```scss 69 | .range-value { 70 | font-size: range(2rem, 6rem); 71 | } 72 | ``` 73 | 74 | #### Using ratios 75 | 76 | You can also use ratios to calculate the `$min-value` or `$max-value`. For ratio suggestions use [modularscale.com](https://www.modularscale.com/). 77 | 78 | Scaling up using the golden ratio (`1.618`) from `2rem`. 79 | 80 | ```scss 81 | .range-value { 82 | margin-bottom: range(2rem, 1.618); 83 | } 84 | ``` 85 | 86 | Scaling down using the golden ratio (`1.618`) from `6rem`. 87 | 88 | ```scss 89 | .range-value { 90 | margin-bottom: range(1.618, 6rem); 91 | } 92 | ``` 93 | 94 | In some edge-cases, you may want to reverse the scaling logic, which can be done by passing in a negative ratio. 95 | 96 | For the example below, `$min-value` will be `1.618`x larger than `$max-value`. 97 | 98 | ```scss 99 | .range-value { 100 | margin-bottom: range(-1.618, 2rem); 101 | } 102 | ``` 103 | 104 | #### Shorthand support 105 | 106 | There is shorthand support for `margin`, `margin-inline`, `margin-block` and `padding`, `padding-inline`, `padding-block`. 107 | 108 | ```scss 109 | .range-value { 110 | margin: range(2rem, 6rem) auto; 111 | } 112 | 113 | .range-value { 114 | margin: range(1.618, 10rem) range(1.618, 6rem) 2rem 2rem; 115 | } 116 | ``` 117 | 118 | ### In/Out 119 | 120 | ```scss 121 | .range-value { 122 | font-size: range(2rem, 6rem, 48rem, 87.5rem); 123 | } 124 | 125 | .range-value { 126 | font-size: clamp(2rem, 2rem + (6 - 2) * ((100vw - 48rem) / (87.5 - 48)), 6rem); 127 | } 128 | ``` 129 | 130 | Please note by default postcss-range-vaue uses `clamp()`. Browsers that do not support CSS `clamp()` will not work. You disable the use of `clamp()` by setting `clamp: false` in the config, which will revert back to media queries for older browser support. 131 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import postcss from 'postcss'; 2 | import shorthands from './shorthands'; 3 | 4 | export default postcss.plugin('postcss-range-value', userOpts => { 5 | // config 6 | const defaultOpts = { rootRem: 16, prefix: 'range', screenMin: '48rem', screenMax: '100rem', clamp: true }; 7 | const opts = { ...defaultOpts, ...userOpts }; 8 | const regExp = new RegExp(escapeRegExp(opts.prefix) + '\\s*\\((.*)\\)', 'i'); 9 | 10 | return root => { 11 | root.walkRules(rule => { 12 | rule.walkDecls(decl => { 13 | // return from non-function declaration 14 | if (!regExp.test(decl.value)) { 15 | return; 16 | } 17 | 18 | // get values as array 19 | const values = postcss.list.space(decl.value); 20 | 21 | // get new declaration object using decl.prop and values array 22 | const newDecls = getNewDecls(decl.prop, values); 23 | 24 | // map through new declarations object entries and render 25 | Object.entries(newDecls).map(item => { 26 | // deconstruct prop and value from item 27 | const [prop, value] = item; 28 | 29 | const valueRange = getDeclValues(value, regExp); 30 | 31 | // append decl and return if not a range value 32 | if (!valueRange) { 33 | rule.append(postcss.decl({ prop, value })); 34 | return; 35 | } 36 | 37 | // get array of range value params 38 | const params = postcss.list.comma(valueRange); 39 | 40 | // deconstruct and set opts defaults 41 | const [userMin, userMax, screenMin = opts.screenMin, screenMax = opts.screenMax] = params; 42 | 43 | // create min/max values and work out value if unit is a ratio 44 | const min = isUnitRatio(userMin) ? getMinFromRatio(userMin, userMax) : userMin; 45 | const max = isUnitRatio(userMax) ? getMaxFromRatio(userMin, userMax) : userMax; 46 | 47 | // error reporting 48 | if (isUnitRatio(userMin) && isUnitRatio(userMax)) { 49 | throw decl.error('Range value requires a unit type for the minimum or maximum size.'); 50 | } 51 | 52 | if (!max) { 53 | throw decl.error('Range value requires a maximum unit size.'); 54 | } 55 | 56 | if (!screenMin) { 57 | throw decl.error('Range value requires a minimum screen size.'); 58 | } 59 | 60 | if (!screenMax) { 61 | throw decl.error('Range value requires a maximum screen size.'); 62 | } 63 | 64 | // sizes to rem unit 65 | const sizes = Object.assign({}, ...Object.entries({ min, max, screenMin, screenMax }).map(([k, v]) => ({ [k]: getUnitRem(v, opts.rootRem) }))); 66 | 67 | // render 68 | if (opts.clamp && getNumber(sizes.min) <= getNumber(sizes.max)) { 69 | // clamp() 70 | // https://caniuse.com/#feat=css-math-functions 71 | // chrome 79+, ff 75+, edge 79+, opera 66+, andriod browser 80+ 72 | rule.append(postcss.decl({ prop, value: `clamp(${sizes.min}, ${sizes.min} + (${getNumber(sizes.max)} - ${getNumber(sizes.min)}) * ((100vw - ${sizes.screenMin}) / (${getNumber(sizes.screenMax)} - ${getNumber(sizes.screenMin)})), ${sizes.max})` })); 73 | } 74 | 75 | else { 76 | // min size 77 | rule.append(postcss.decl({ prop, value: sizes.min })); 78 | 79 | // vw based size 80 | rule.vw = postcss.atRule({ name: 'media', params: `(min-width: ${sizes.screenMin})` }); 81 | 82 | rule 83 | .vw 84 | .append({ selector: rule.selector }) 85 | .walkRules(selector => { 86 | selector.append({ 87 | prop, 88 | value: `calc(${sizes.min} + (${getNumber(sizes.max)} - ${getNumber(sizes.min)}) * ((100vw - ${sizes.screenMin}) / (${getNumber(sizes.screenMax)} - ${getNumber(sizes.screenMin)})))`, 89 | }); 90 | }); 91 | 92 | root.insertAfter(rule, rule.vw); 93 | 94 | // max size 95 | rule.max = postcss.atRule({ name: 'media', params: `(min-width: ${sizes.screenMax})` }); 96 | 97 | rule 98 | .max 99 | .append({selector: rule.selector}) 100 | .walkRules(selector => { 101 | selector.append({ 102 | prop, 103 | value: sizes.max, 104 | }); 105 | }); 106 | 107 | root.insertAfter(rule.vw, rule.max); 108 | } 109 | }); 110 | 111 | decl.remove(); 112 | }); 113 | }); 114 | }; 115 | }); 116 | 117 | // return new declarations as object with k => v 118 | const getNewDecls = (prop, values) => { 119 | return shorthands(prop, values) || { [prop]: values.shift() }; 120 | } 121 | 122 | // get declaration values 123 | const getDeclValues = (value, regExp) => { 124 | const match = value.match(regExp); 125 | return match ? match[1] : false; 126 | } 127 | 128 | // get min value from min ratio value 129 | const getMinFromRatio = (min, max) => { 130 | // if negative value then reverse logic 131 | return (min < 0 ? getNumber(max) * (min * -1) : getNumber(max) / min) + getUnit(max); 132 | } 133 | 134 | // get max value from max ratio value 135 | const getMaxFromRatio = (min, max) => { 136 | // if negative value then reverse logic 137 | return (max < 0 ? getNumber(min) / (max * -1) : getNumber(min) * max) + getUnit(min); 138 | } 139 | 140 | // is unit a ratio 141 | const isUnitRatio = x => { 142 | return getUnit(x) === ''; 143 | } 144 | 145 | // get unit only 146 | const getUnit = x => { 147 | return x.replace(/^-?[0-9.]+/g, ''); 148 | } 149 | 150 | // get number only 151 | const getNumber = x => { 152 | return x.replace(/[a-zA-Z]+/g, ''); 153 | } 154 | 155 | // get rem from px value 156 | const getUnitRem = (x, rootRem) => { 157 | return getUnit(x) === 'px' ? `${getNumber(x) / rootRem}rem` : x; 158 | } 159 | 160 | // esc reg exp function for prefix 161 | const escapeRegExp = str => { 162 | return str.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string 163 | } 164 | --------------------------------------------------------------------------------