CSS class

The canonical container-side pattern. Put optical-center: auto on a CSS class, attach that class to a wrapper, drop the icon inside.

When to use it

This is the default. Reach for any other pattern only when you have a reason to.

  • You already have a CSS file (or CSS module) for the component.
  • You're rendering icons through a component library — lucide-react, react-icons, your own wrapper.
  • You want zero markup change at the icon itself.

How to write it

One declaration in CSS, one className on the wrapper, the icon as a child:

styles/icons.css css
.badge {
  optical-center: auto;
}
Button.tsx tsx
import { Play } from 'lucide-react';

export function PlayButton() {
  return (
    <button>
      <div className="badge">
        <Play />
      </div>
    </button>
  );
}

Notice the directive is on .badge — the wrapper. The <Play /> component is untouched. You never set optical-center on the icon.

What gets emitted

At build time the PostCSS plugin walks every CSS rule with optical-center: auto, scans the project's JSX for any element whose className matches the rule's selector, and resolves the icon component to its source SVG. It then emits:

dist/icons.css css
.badge {
  display: flex;
  --optical-center: auto;
}
.badge > * {
  margin: auto;
  translate: 4.3365% 2.604%;
}

Three things to notice about the output:

  • Centering is via flex auto-margin. The plugin sets display: flex on the wrapper and margin: auto on the child — the well-known flex auto-margin trick that absorbs free space along both axes. No justify-content, no align-items.
  • The perceptual shift is a translate. The exact translate: dx% dy% value comes from running the icon's source SVG through the centering pipeline once at build time. The number is per-icon and lives only where you'd expect it — next to the asset it was computed from.
  • --optical-center: auto is a DevTools tracer. Browsers ignore unknown custom property values, but the line stays visible in the inspector so you can confirm the rule was processed even when the visible change is small.

How the plugin finds the icon

The PostCSS plugin scans every .jsx and .tsx file in your project at build time (once, cached). It knows lucide-react out of the box — <Play />, <Heart />, any of its named exports map to the matching SVG in the lucide-static package. No config needed.

When the plugin then finds a CSS rule with optical-center: auto, it walks the rule's selectors left-to-right, matches the first class token that wraps a tracked icon, and uses that icon's SVG to compute the offset.

Requirements

  • The wrapper rule must not declare align-items or justify-content. The directive is what replaces them; mixing fights the auto-margin.
  • The icon child must stay in normal flow. Don't position: absolute it — the auto-margin technique needs a flex item.
  • The wrapper must contain exactly one icon child. Multiple children all pick up margin: auto, which produces unexpected layouts.