CSS mask-image

A utility class mounts the SVG via mask-image and recolors it with currentColor. optical-center: auto on the same rule rewrites the underlying SVG and inlines it as a data: URI.

When to use it

Pick this when the icon is delivered as a CSS mask — typically when you want every icon to inherit currentColor so the same class can render in any text color without per-icon variants.

  • shadcn-style icon primitives.
  • Tailwind mask-utility stacks.
  • Anywhere an icon ships as a one-color mask on top of a colored background.

How to write it

Mount the SVG via mask, paint it with background, and add optical-center: auto to the same rule. The rule's element is the icon — there's no wrapper here.

icons.css css
.icon-play {
  display: inline-block;
  width: 24px;
  height: 24px;
  background: currentColor;
  mask: url('lucide-static/icons/play.svg') center / contain no-repeat;
  optical-center: auto;
}
Button.tsx tsx
<button>
  <span className="icon-play" />
</button>

What gets emitted

The PostCSS plugin reads each url('…svg') in the rule, rasterizes the file, runs the centering pipeline, rewrites the SVG's viewBox, and inlines the result as a data:image/svg+xml,… URI in place of the original URL.

dist/icons.css css
.icon-play {
  display: inline-block;
  width: 24px;
  height: 24px;
  background: currentColor;
  mask: url('data:image/svg+xml;utf8,…rewritten…') center / contain no-repeat;
  --optical-center: auto;
}

Two things to notice:

  • The shift lives in the mask asset. The data URI contains an SVG with its viewBox already corrected. The browser sees a flat, pre-shifted icon and does no runtime work.
  • No positioning was emitted. Unlike the wrapper patterns, this rule doesn't pick up display: flex or translate — the perceptual shift is internal to the asset, so the element's position is entirely the consumer's layout concern. Drop it in a flex item, a grid cell, a button, anywhere.

URL resolution

The plugin resolves URLs in this order:

  1. data: URIs and absolute http(s): URLs are skipped.
  2. Any matching aliases prefix you configured gets expanded.
  3. Absolute paths and ./ / ../ paths resolve against the CSS file's directory.
  4. Bare specifiers like lucide-static/icons/play.svg go through Node's module resolution against the CSS file's directory. That makes installed npm icon packages work without alias config.
postcss.config.js js
import opticalCenter from 'optical-center/postcss';

export default {
  plugins: [
    opticalCenter({
      aliases: {
        '@icons': '/abs/path/to/icons',
      },
    }),
  ],
};

Centering is the consumer's job

Because this rule doesn't emit positioning, the element it targets needs to be centered however the surrounding layout normally centers things. Inside a flex parent, that's justify-content + align-items on the parent, or margin: auto on the child — both work because the mask's viewBox already carries the shift.