postcss-rule-unit-converter
Rule-driven PostCSS unit conversion plugin.
- Cookbook: COOKBOOK.md
- API Reference: API.md
- Migration Guide: MIGRATION.md
- Preset Authoring: PRESET_AUTHORING.md
Features
- One plugin for
rpx,px,rem,vw,vh, and custom unit transforms - Order-based matching rules
- Built-in common presets for two-way conversion between common units
- Reuses the same filtering options as the other packages in this monorepo
- Supports single presets and grouped preset collections for common workflows
Mental Model
- Each rule matches a source unit and produces a next value.
- Rules are evaluated in order.
- If two rules can match the same source unit, the first matching rule wins.
- Use
composeRules(...)when you want to merge multiple preset groups and custom rules into one ordered rule list.
Usage
import postcss from 'postcss'
import unitConverter, { composeRules, presets } from 'postcss-rule-unit-converter'
const result = await postcss([
unitConverter({
rules: composeRules(
presets.remToViewport({ viewportWidth: 375 }),
presets.pxToRem({ rootValue: 16 }),
),
}),
]).process('.title{font-size:1rem;margin:16px}', { from: undefined })Grouped Presets
import postcss from 'postcss'
import unitConverter, { composeRules, presets } from 'postcss-rule-unit-converter'
const result = await postcss([
unitConverter({
rules: composeRules(
presets.rpxPresetGroup({
ratio: 2,
rootValue: 16,
viewportWidth: 375,
viewportHeight: 667,
}),
{
from: 'em',
to: 'rpx',
factor: 32,
},
),
}),
]).process('.demo{font-size:32rpx;left:10vw}', { from: undefined })Available grouped presets:
presets.rpxPresetGroup()normalizespx/rem/vw/vhintorpxpresets.pxPresetGroup()normalizesrem/rpx/vw/vhintopxpresets.viewportPresetGroup()normalizespx/rem/rpxinto eithervworvhpresets.webPresetGroup()bundles commonpx/rem/vw/vh/rpxweb-style conversions
Migrating From postcss-units-to-px
presets.unitsToPx() accepts the same unitMap shapes used by postcss-units-to-px:
import postcss from 'postcss'
import unitConverter, { presets } from 'postcss-rule-unit-converter'
const result = await postcss([
unitConverter({
propList: ['*'],
rules: presets.unitsToPx({
unitMap: {
rem: 16,
vw: false,
foo: null,
},
transform(value, unit, context) {
return unit === 'foo' && context.prop === 'margin' ? value * 10 : undefined
},
}),
}),
]).process('.demo{font-size:1rem;width:1vw;margin:2foo}', { from: undefined })Object unitMap values merge over the default rem/em/vw/vh/vmin/vmax/rpx map. Map and array unitMap values preserve your matcher order and do not merge defaults. Use false to skip a unit and null to fall back to transform(value, unit, context).
More Examples
import postcss from 'postcss'
import unitConverter, { presets } from 'postcss-rule-unit-converter'
const result = await postcss([
unitConverter({
rules: [
presets.rpxToPx(),
presets.pxToRpx(),
presets.rpxToRem({ rootValue: 16 }),
presets.rpxToVw({ viewportWidth: 375 }),
presets.rpxToVh({ viewportHeight: 667 }),
presets.vwToPx({ viewportWidth: 375 }),
presets.vhToPx({ viewportHeight: 667 }),
],
}),
]).process('.demo{font-size:32rpx;width:10vw;height:10vh}', { from: undefined })Custom Presets
Single preset with exported types:
import type { PresetFactory, RemBasedPresetOptions } from 'postcss-rule-unit-converter'
import postcss from 'postcss'
import unitConverter, { definePreset } from 'postcss-rule-unit-converter'
const remToDp: PresetFactory<RemBasedPresetOptions> = definePreset((options = {}) => {
const { rootValue = 16, minValue, to = 'dp' } = options
return {
from: 'rem',
to,
minValue,
transform: (value, context) => {
const resolvedRootValue = typeof rootValue === 'function'
? rootValue(context.input)
: rootValue
return value * resolvedRootValue
},
}
})
const result = await postcss([
unitConverter({
rules: [remToDp({ rootValue: 20 })],
}),
]).process('.demo{font-size:1rem}', { from: undefined })Grouped preset with exported types:
import type { PresetGroupFactory, RemBasedPresetOptions } from 'postcss-rule-unit-converter'
import postcss from 'postcss'
import unitConverter, { definePresetGroup } from 'postcss-rule-unit-converter'
type MyPresetOptions = RemBasedPresetOptions & {
ratio?: number
}
const mobilePresetGroup: PresetGroupFactory<MyPresetOptions> = definePresetGroup((options = {}) => {
const { rootValue = 16, ratio = 2 } = options
return [
{
from: 'rem',
to: 'rpx',
transform: (value, context) => {
const resolvedRootValue = typeof rootValue === 'function'
? rootValue(context.input)
: rootValue
return value * resolvedRootValue * ratio
},
},
{
from: 'px',
to: 'rpx',
factor: ratio,
},
]
})
const result = await postcss([
unitConverter({
rules: mobilePresetGroup({ rootValue: 16, ratio: 2 }),
}),
]).process('.demo{font-size:1rem;margin:16px}', { from: undefined })Custom transform rule:
import postcss from 'postcss'
import unitConverter from 'postcss-rule-unit-converter'
const result = await postcss([
unitConverter({
rules: [
{
from: /^x$/,
to: 'px',
transform(value, context) {
return context.prop === 'letter-spacing' ? value * 4 : value * 8
},
},
],
}),
]).process('.demo{letter-spacing:2x;margin:2x}', { from: undefined })Raw match context for advanced transforms:
import postcss from 'postcss'
import unitConverter from 'postcss-rule-unit-converter'
const result = await postcss([
unitConverter({
rules: [
{
from: /^(px)$/i,
to: 'px',
transform(value, context) {
if (context.rawUnit === 'PX') {
return {
value,
unit: 'ch',
}
}
return Number(context.rawValue) / 2
},
},
],
}),
]).process('.demo{width:40PX;height:40px}', { from: undefined })RuleContext extends the shared replace context and also exposes:
fromUnit: source unit used for rule matching; it is normalized to lowercase when the plugin uses a custom or broad unit regexrawUnit: matched unit text before normalization, for examplePXrawValue: matched numeric text beforeNumber(...), for example40match: full matched fragment, for example40PX
Advanced matching options:
unitRegex: override the generated matching regex when you need custom parsing behavior such as matching insidevar(...)keepZeroUnit: keep0px,0rem, or custom zero outputs instead of collapsing them to bare0
String unit matchers are trimmed and normalized to lowercase. With the default generated regex, 'px' matches lowercase px; use /^px$/i or a custom unitRegex if you need uppercase unit spellings. The default regex skips quoted strings, url(...), and var(...); a custom unitRegex replaces that regex, so include those skip branches yourself if you still need them.
Rule Tips
- Use
replace: falsewhen you want to keep fallback declarations beside converted values. - Use
propListto keep different presets from competing on the same source unit in unrelated properties. - Prefer grouped presets when your project has one main unit system.
- Prefer explicit custom rule arrays when you need tight control over rule order.
- For
presets.viewportPresetGroup(), chooseviewportUnit: 'vw'orviewportUnit: 'vh'explicitly when you want one axis. - Prefer
definePreset(...)anddefinePresetGroup(...)when authoring reusable presets in your own package or app config. - Use
RuleContext.rawUnit,RuleContext.rawValue, andRuleContext.matchwhen your transform depends on original casing or exact matched text. - Use
unitRegexwhen you need non-default matching behavior such as not skippingvar(...)fallbacks. - Use
keepZeroUnitwhen downstream tooling expects0to keep its explicit unit. - If you want more scenario-based examples, see the cookbook.
Presets
Custom preset authoring helpers:
definePreset()definePresetGroup()type PresetFactory<TOptions>type PresetGroupFactory<TOptions>presets.remToPx()presets.remToRpx()presets.remToRpxRatio()presets.remToRpxByRatio()presets.remToResponsivePixel()presets.remToViewport()presets.remToVw()presets.remToVh()presets.pxToRem()presets.pxToViewport()presets.pxToVw()presets.pxToVh()presets.pxToRpx()presets.rpxToPx()presets.rpxToRem()presets.rpxToVw()presets.rpxToVh()presets.vwToPx()presets.vhToPx()presets.vwToRem()presets.vhToRem()presets.vwToRpx()presets.vhToRpx()presets.pxPresetGroup()presets.rpxPresetGroup()presets.viewportPresetGroup()presets.webPresetGroup()presets.unitsToPx()