Direction Attribute Missing
Ensure dir="ltr" or dir="rtl" is set on your HTML element
The plugin works out of the box with zero configuration, but offers powerful customization options for advanced use cases.
module.exports = { plugins: [ require('postcss-logical-polyfill')() ]}const logicalPolyfill = require('postcss-logical-polyfill');
module.exports = { plugins: [ logicalPolyfill({ // Direction selectors (default shown) rtl: { selector: '[dir="rtl"]' }, ltr: { selector: '[dir="ltr"]' },
// Output order for unscoped properties outputOrder: 'ltr-first' // or 'rtl-first' }) ]}Control how the plugin generates direction-specific selectors:
{ rtl: { selector: '[dir="rtl"]' }, ltr: { selector: '[dir="ltr"]' }}{ rtl: { selector: '.rtl' }, ltr: { selector: '.ltr' }}{ rtl: { selector: '[data-theme="rtl"]' }, ltr: { selector: '[data-theme="ltr"]' }}Control the order of generated rules for unscoped properties:
{ outputOrder: 'ltr-first' // Default: LTR rules first, then RTL // outputOrder: 'rtl-first' // RTL rules first, then LTR}Example Output:
.container { margin-left: 1rem; margin-right: 1rem;}[dir="ltr"] .container { padding-left: 1rem;}[dir="rtl"] .container { padding-right: 1rem;}.container { margin-left: 1rem; margin-right: 1rem;}[dir="rtl"] .container { padding-right: 1rem;}[dir="ltr"] .container { padding-left: 1rem;}Use a PostCSS configuration rather than adding another CSS loader to Next.js:
export default { plugins: { 'postcss-logical-polyfill': {} }};A custom PostCSS configuration replaces Next.js defaults. Include the other plugins your application needs; run nesting expansion and any CSS-producing framework plugin before this plugin. Set the root HTML dir attribute in your layout.
import { defineConfig } from 'vite';import logicalPolyfill from 'postcss-logical-polyfill';
export default defineConfig({ css: { postcss: { plugins: [ logicalPolyfill({ outputOrder: 'ltr-first' }) ] } }});module.exports = { module: { rules: [ { test: /\.css$/, use: [ 'style-loader', 'css-loader', { loader: 'postcss-loader', options: { postcssOptions: { plugins: [ require('postcss-logical-polyfill')({ rtl: { selector: '.rtl' }, ltr: { selector: '.ltr' } }) ] } } } ] } ] }};Run this plugin after Tailwind generates its CSS so that generated logical declarations can be transformed. Configure the Tailwind PostCSS plugin for your installed major version (tailwindcss for v3, @tailwindcss/postcss for v4). Keep any other processors required by your target browsers.
This plugin transforms logical properties; it does not mirror physical utilities such as ml-* and mr-*. Prefer logical utilities when you need direction-aware layouts.
For complex scenarios, you can use advanced selector patterns:
{ rtl: { selector: 'html[dir="rtl"]', // More specific }, ltr: { selector: 'html[dir="ltr"]', }}{ rtl: { selector: '[dir="rtl"], .rtl-mode' // Multiple selectors }, ltr: { selector: '[dir="ltr"], .ltr-mode' }}You must set the dir attribute on your HTML for the generated CSS to work:
<!DOCTYPE html><html dir="ltr"> <!-- For left-to-right layouts --><head> <meta charset="UTF-8"> <title>My App</title></head><body> <!-- Your content --></body></html>For RTL languages:
<html dir="rtl"> <!-- For right-to-left layouts -->For applications that support dynamic direction switching:
// JavaScript to toggle directionfunction toggleDirection() { const html = document.documentElement; const currentDir = html.getAttribute('dir'); html.setAttribute('dir', currentDir === 'rtl' ? 'ltr' : 'rtl');}import { useEffect, useState } from 'react';
function App() { const [direction, setDirection] = useState('ltr');
useEffect(() => { document.documentElement.setAttribute('dir', direction); }, [direction]);
return ( <div> <button onClick={() => setDirection(direction === 'ltr' ? 'rtl' : 'ltr')}> Switch to {direction === 'ltr' ? 'RTL' : 'LTR'} </button> {/* Your app content */} </div> );}The plugin automatically optimizes block-direction properties to generate fewer rules:
/* Input */.element { margin-block: 1rem; padding-block-start: 0.5rem;}
/* Optimized Output - No direction selectors needed */.element { margin-top: 1rem; margin-bottom: 1rem; padding-top: 0.5rem;}For large codebases, consider processing only files that contain logical properties:
// Only process certain file patternsmodule.exports = { plugins: [ // Other plugins require('postcss-logical-polyfill')() ]};You can debug the transformation process by examining the generated CSS:
const postcss = require('postcss');const logicalPolyfill = require('postcss-logical-polyfill');
const result = await postcss([logicalPolyfill()]) .process(css, { from: undefined });
console.log('Original CSS:', css);console.log('Transformed CSS:', result.css);Direction Attribute Missing
Ensure dir="ltr" or dir="rtl" is set on your HTML element
Selector Specificity
Check that your direction selectors have appropriate specificity
Order Matters
Consider the outputOrder option for consistent rule ordering
dir attribute on your <html> element/* Before: Physical properties */.element { margin-left: 1rem; margin-right: 2rem; padding-left: 0.5rem;}
/* After: Logical properties */.element { margin-inline: 1rem 2rem; padding-inline-start: 0.5rem;}Like postcss-logical, this plugin converts logical properties to physical properties. It generates both horizontal LTR and RTL scopes in one stylesheet, rather than mapping all declarations for one configured direction. Review the generated selectors, specificity and supported properties when migrating.
Enable animation compilation explicitly:
logicalPolyfill({ animations: true });The default is false. Referenced logical keyframes receive separate physical-property copies for LTR and RTL, with collision-free generated names. Static animation-name and animation shorthand references use the matching direction copy. Original definitions remain available. Duplicate names in conditional rules retain those conditions.
Definitions and references must be available in the same PostCSS root; run postcss-import first for multi-file animations. Expand CSS nesting first. Dynamic names in var()/env() remain unchanged with a warning and do not receive an old-browser fallback.