Skip to content

postcss-rule-unit-converter

Rule-driven PostCSS unit conversion plugin.

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

ts
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

ts
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() normalizes px/rem/vw/vh into rpx
  • presets.pxPresetGroup() normalizes rem/rpx/vw/vh into px
  • presets.viewportPresetGroup() normalizes px/rem/rpx into either vw or vh
  • presets.webPresetGroup() bundles common px/rem/vw/vh/rpx web-style conversions

Migrating From postcss-units-to-px

presets.unitsToPx() accepts the same unitMap shapes used by postcss-units-to-px:

ts
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

ts
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:

ts
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:

ts
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:

ts
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:

ts
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 regex
  • rawUnit: matched unit text before normalization, for example PX
  • rawValue: matched numeric text before Number(...), for example 40
  • match: full matched fragment, for example 40PX

Advanced matching options:

  • unitRegex: override the generated matching regex when you need custom parsing behavior such as matching inside var(...)
  • keepZeroUnit: keep 0px, 0rem, or custom zero outputs instead of collapsing them to bare 0

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: false when you want to keep fallback declarations beside converted values.
  • Use propList to 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(), choose viewportUnit: 'vw' or viewportUnit: 'vh' explicitly when you want one axis.
  • Prefer definePreset(...) and definePresetGroup(...) when authoring reusable presets in your own package or app config.
  • Use RuleContext.rawUnit, RuleContext.rawValue, and RuleContext.match when your transform depends on original casing or exact matched text.
  • Use unitRegex when you need non-default matching behavior such as not skipping var(...) fallbacks.
  • Use keepZeroUnit when downstream tooling expects 0 to 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()

Built for CSS that moves between screens.