Skip to main content

eslint-plugin-logical-properties

Transform your normal TS/JS-based CSS into RTL-ready CSS (think: vanilla extract or JSX style attribute).

This works by targeting certain JSX attributes and TS/JS function calls to identify what objects need transforming. Needless to say, this works on pure TS/JS Objects.

Usage​

[!WARNING] This plugin will only work with a flat config ESLint (please upgrade, it's great!)

As with any eslint plugin, it comes with presets and ways of adapting it to your own needs.

Basic Setup (Preset Only)​

The simplest way to use this plugin is with one of the presets:

import logicalPropertiesPlugin from 'eslint-plugin-logical-properties';

const finalEslintConfig = [
logicalPropertiesPlugin.configs.recommended,
// logicalPropertiesPlugin.configs.warning,
// logicalPropertiesPlugin.configs.disable,
];

Available presets:

  • recommended (all rules turned on, error severity)
  • warning (all rules set to warn severity)
  • disable (all rules turned off)

These presets include the plugin and default settings automatically.

Advanced Setup (Preset + Custom Settings)​

import logicalPropertiesPlugin from 'eslint-plugin-logical-properties';

const customConfig = [
{
name: 'logical-properties',
plugins: {
'logical-properties': logicalPropertiesPlugin,
},
settings: {
'logical-properties': {
functions: ['style', 'globalStyle', 'myStyleFactory'],
keyframes: ['keyframes', 'myKeyframes'],
jsxAttributes: ['style', 'sx'],
resolvers: ['selectors.*', '@media.*', '@supports.*', 'variants.*.*', 'custom.tokens.*'],
},
},
rules: {
...logicalPropertiesPlugin.configs.recommended.rules,
},
},
];

Per-Rule Override​

If one rule needs custom behavior, rule options override shared settings:

import logicalPropertiesPlugin from 'eslint-plugin-logical-properties';

const finalEslintConfig = [
logicalPropertiesPlugin.configs.recommended,
{
name: 'logical-properties-overrides',
rules: {
'logical-properties/padding': ['error', {
functions: ['myStyleFactory'],
resolvers: ['custom.tokens.*'],
}],
},
},
];

From Scratch (No Preset)​

For complete control, you can build the config manually without presets:

import logicalPropertiesPlugin from 'eslint-plugin-logical-properties';

const finalEslintConfig = [
{
name: 'logical-properties',
plugins: {
'logical-properties': logicalPropertiesPlugin,
},
settings: {
'logical-properties': {
functions: ['style', 'globalStyle'],
keyframes: ['keyframes'],
jsxAttributes: ['style'],
resolvers: ['selectors.*', '@media.*'],
},
},
rules: {
'logical-properties/padding': 'error',
'logical-properties/margin': 'error',
'logical-properties/inset': 'error',
// ... add other rules as needed
},
},
];

Custom Configuration​

If you want explicit all-rules overrides in one place, use createLogicalPropertiesConfig. This helper keeps its previous behavior and generates a complete ESLint config.

  • helper-based all-rules config:
import { createLogicalPropertiesConfig } from 'eslint-plugin-logical-properties';

const config = createLogicalPropertiesConfig('error', {
functions: ['style', 'globalStyle'],
keyframes: ['keyframes'],
jsxAttributes: ['style'],
resolvers: ['selectors.*', '@media.*'],
});

[!NOTE] The helper generates an entire config, with the plugin attached, not just rules, so you can use it directly in your ESLint config.

Configuration Priority​

Option resolution happens in this order:

  1. Internal defaults
  2. Shared plugin settings (settings['logical-properties'])
  3. Per-rule options (rules['logical-properties/<rule>'][1])

Configuration​

All rules share the same options:

  • functions
  • keyframes
  • jsxAttributes
  • resolvers

The difference lies in how they are processed:

  • functions: expected to be called with CSS objects or arrays of CSS objects (ignores all else, like strings)
  • keyframes: expected to be called with an object whose properties are CSS objects
  • jsxAttributes: attributes on your JSX components that should be treated as CSS objects
  • resolvers: nested object paths to recursively inspect for CSS objects

[!NOTE] When referring to CSS Objects, it's understood as an object whose keys are CSS properties

Default Config​

const options = {
functions: ['style', 'globalStyle', 'recipe', 'sprinkles', 'defineProperties'],
keyframes: ['keyframes'],
jsxAttributes: ['style'],
resolvers: [
'selectors.*',
'@media.*',
'@supports.*',
'base',
'variants.*.*',
'compoundVariants.*.style',
],
}

// Helper
const generatedConfig = createLogicalPropertiesConfig('error', options);

// Manual Config
const manualConfig = {
rules: {
'logical-properties/padding': ['error', options]
}
};

Shorthand Behavior​

For shorthand rules (padding, margin, borderRadius), fixes are conservative:

  • 1 value: unchanged
  • 2 values: rewritten to logical pair properties
  • 3 values: expanded to block-start, inline (single), block-end
  • 4 values: expanded to primitive logical properties

Examples:

// 1 value -> unchanged
style({ padding: '8px' })

// 2 values -> logical pair
style({ padding: '8px 16px' })
// becomes
style({ paddingBlock: '8px', paddingInline: '16px' })

// 3 values -> block-start, single inline, block-end
style({ padding: '8px 16px 4px' })
// becomes
style({
paddingBlockStart: '8px',
paddingInline: '16px',
paddingBlockEnd: '4px',
})

// 4 values -> primitive logical properties
style({ padding: '8px 16px 4px 12px' })
// becomes
style({
paddingBlockStart: '8px',
paddingInlineEnd: '16px',
paddingBlockEnd: '4px',
paddingInlineStart: '12px',
})

The same logic also works for quoted strings and template strings.

Migration Notes​

If you are upgrading from a version that always expanded shorthand values to four properties, the biggest changes are two-value and three-value shorthand behavior.

Before/After Autofix​

// Before (old behavior - 2 values)
style({ padding: '8px 16px' })
// autofix
style({
paddingBlockStart: '8px',
paddingBlockEnd: '8px',
paddingInlineStart: '16px',
paddingInlineEnd: '16px',
})

// After (current behavior - 2 values)
style({ padding: '8px 16px' })
// autofix
style({
paddingBlock: '8px',
paddingInline: '16px',
})
// Before (old behavior - 3 values)
style({ padding: '8px 16px 4px' })
// autofix
style({
paddingBlockStart: '8px',
paddingInlineStart: '16px',
paddingInlineEnd: '16px',
paddingBlockEnd: '4px',
})

// After (current behavior - 3 values)
style({ padding: '8px 16px 4px' })
// autofix
style({
paddingBlockStart: '8px',
paddingInline: '16px',
paddingBlockEnd: '4px',
})
// Before and after for one-value shorthand
style({ padding: '8px' })
// no autofix
style({ padding: '8px' })
// Before and after for 4 values
style({ padding: '8px 16px 4px 12px' })
// autofix (unchanged)
style({
paddingBlockStart: '8px',
paddingInlineEnd: '16px',
paddingBlockEnd: '4px',
paddingInlineStart: '12px',
})

Rules​

RuleSourceReplacementType
borderborderLeftborderInlineStartproperty
borderborderLeftColorborderInlineStartColorproperty
borderborderLeftStyleborderInlineStartStyleproperty
borderborderLeftWidthborderInlineStartWidthproperty
borderborderRightborderInlineEndproperty
borderborderRightColorborderInlineEndColorproperty
borderborderRightStyleborderInlineEndStyleproperty
borderborderRightWidthborderInlineEndWidthproperty
borderborderTopborderBlockStartproperty
borderborderTopColorborderBlockStartColorproperty
borderborderTopStyleborderBlockStartStyleproperty
borderborderTopWidthborderBlockStartWidthproperty
borderborderBottomborderBlockEndproperty
borderborderBottomColorborderBlockEndColorproperty
borderborderBottomStyleborderBlockEndStyleproperty
borderborderBottomWidthborderBlockEndWidthproperty
borderborderborderInlineStart, borderInlineEnd, borderBlockStart, borderBlockEndshorthand mapping
border-radiusborderTopLeftRadiusborderStartStartRadiusproperty
border-radiusborderTopRightRadiusborderStartEndRadiusproperty
border-radiusborderBottomLeftRadiusborderEndStartRadiusproperty
border-radiusborderBottomRightRadiusborderEndEndRadiusproperty
border-radiusborderRadius (2 values)borderStartStartRadius + borderEndEndRadius, borderStartEndRadius + borderEndStartRadiusshorthand pair mapping
border-radiusborderRadius (3-4 values)primitive logical radius propertiesshorthand expansion
insettopinsetBlockStartproperty
insetbottominsetBlockEndproperty
insetleftinsetInlineStartproperty
insetrightinsetInlineEndproperty
marginmarginLeftmarginInlineStartproperty
marginmarginRightmarginInlineEndproperty
marginmarginTopmarginBlockStartproperty
marginmarginBottommarginBlockEndproperty
marginmargin (2 values)marginBlock, marginInlineshorthand pair mapping
marginmargin (3 values)marginBlockStart, marginInline, marginBlockEndshorthand 3-value
marginmargin (4 values)primitive logical margin propertiesshorthand expansion
paddingpaddingLeftpaddingInlineStartproperty
paddingpaddingRightpaddingInlineEndproperty
paddingpaddingToppaddingBlockStartproperty
paddingpaddingBottompaddingBlockEndproperty
paddingpadding (2 values)paddingBlock, paddingInlineshorthand pair mapping
paddingpadding (3 values)paddingBlockStart, paddingInline, paddingBlockEndshorthand 3-value
paddingpadding (4 values)primitive logical padding propertiesshorthand expansion
overflowoverflowXoverflowInlineproperty
overflowoverflowYoverflowBlockproperty
overscroll-behavioroverscrollBehaviorXoverscrollBehaviorInlineproperty
overscroll-behavioroverscrollBehaviorYoverscrollBehaviorBlockproperty
sizewidthinlineSizeproperty
sizeheightblockSizeproperty
sizeminWidthminInlineSizeproperty
sizeminHeightminBlockSizeproperty
sizemaxWidthmaxInlineSizeproperty
sizemaxHeightmaxBlockSizeproperty
floatfloat: leftfloat: inline-startvalue
floatfloat: rightfloat: inline-endvalue
clearclear: leftclear: inline-startvalue
clearclear: rightclear: inline-endvalue

Index

Variables

constdefault

default: { configs: { disable: { plugins: { logical-properties: { meta: { name: string; version: string }; rules: { border: RuleModule; border-radius: RuleModule; clear: RuleModule; float: RuleModule; inset: RuleModule; margin: RuleModule; overflow: RuleModule; overscroll-behavior: RuleModule; padding: RuleModule; size: RuleModule } } }; rules: {}; settings: { logical-properties: { functions: readonly [style, globalStyle, recipe, sprinkles, defineProperties]; jsxAttributes: readonly [style]; keyframes: readonly [keyframes]; resolvers: readonly [selectors.*, @media.*, @supports.*, base, variants.*.*, compoundVariants.*.style] } } }; recommended: { plugins: { logical-properties: { meta: { name: string; version: string }; rules: { border: RuleModule; border-radius: RuleModule; clear: RuleModule; float: RuleModule; inset: RuleModule; margin: RuleModule; overflow: RuleModule; overscroll-behavior: RuleModule; padding: RuleModule; size: RuleModule } } }; rules: {}; settings: { logical-properties: { functions: readonly [style, globalStyle, recipe, sprinkles, defineProperties]; jsxAttributes: readonly [style]; keyframes: readonly [keyframes]; resolvers: readonly [selectors.*, @media.*, @supports.*, base, variants.*.*, compoundVariants.*.style] } } }; warning: { plugins: { logical-properties: { meta: { name: string; version: string }; rules: { border: RuleModule; border-radius: RuleModule; clear: RuleModule; float: RuleModule; inset: RuleModule; margin: RuleModule; overflow: RuleModule; overscroll-behavior: RuleModule; padding: RuleModule; size: RuleModule } } }; rules: {}; settings: { logical-properties: { functions: readonly [style, globalStyle, recipe, sprinkles, defineProperties]; jsxAttributes: readonly [style]; keyframes: readonly [keyframes]; resolvers: readonly [selectors.*, @media.*, @supports.*, base, variants.*.*, compoundVariants.*.style] } } } }; meta: { name: string; version: string }; rules: { border: RuleModule; border-radius: RuleModule; clear: RuleModule; float: RuleModule; inset: RuleModule; margin: RuleModule; overflow: RuleModule; overscroll-behavior: RuleModule; padding: RuleModule; size: RuleModule } } = ...

Type declaration

  • configs: { disable: { plugins: { logical-properties: { meta: { name: string; version: string }; rules: { border: RuleModule; border-radius: RuleModule; clear: RuleModule; float: RuleModule; inset: RuleModule; margin: RuleModule; overflow: RuleModule; overscroll-behavior: RuleModule; padding: RuleModule; size: RuleModule } } }; rules: {}; settings: { logical-properties: { functions: readonly [style, globalStyle, recipe, sprinkles, defineProperties]; jsxAttributes: readonly [style]; keyframes: readonly [keyframes]; resolvers: readonly [selectors.*, @media.*, @supports.*, base, variants.*.*, compoundVariants.*.style] } } }; recommended: { plugins: { logical-properties: { meta: { name: string; version: string }; rules: { border: RuleModule; border-radius: RuleModule; clear: RuleModule; float: RuleModule; inset: RuleModule; margin: RuleModule; overflow: RuleModule; overscroll-behavior: RuleModule; padding: RuleModule; size: RuleModule } } }; rules: {}; settings: { logical-properties: { functions: readonly [style, globalStyle, recipe, sprinkles, defineProperties]; jsxAttributes: readonly [style]; keyframes: readonly [keyframes]; resolvers: readonly [selectors.*, @media.*, @supports.*, base, variants.*.*, compoundVariants.*.style] } } }; warning: { plugins: { logical-properties: { meta: { name: string; version: string }; rules: { border: RuleModule; border-radius: RuleModule; clear: RuleModule; float: RuleModule; inset: RuleModule; margin: RuleModule; overflow: RuleModule; overscroll-behavior: RuleModule; padding: RuleModule; size: RuleModule } } }; rules: {}; settings: { logical-properties: { functions: readonly [style, globalStyle, recipe, sprinkles, defineProperties]; jsxAttributes: readonly [style]; keyframes: readonly [keyframes]; resolvers: readonly [selectors.*, @media.*, @supports.*, base, variants.*.*, compoundVariants.*.style] } } } }
    • disable: { plugins: { logical-properties: { meta: { name: string; version: string }; rules: { border: RuleModule; border-radius: RuleModule; clear: RuleModule; float: RuleModule; inset: RuleModule; margin: RuleModule; overflow: RuleModule; overscroll-behavior: RuleModule; padding: RuleModule; size: RuleModule } } }; rules: {}; settings: { logical-properties: { functions: readonly [style, globalStyle, recipe, sprinkles, defineProperties]; jsxAttributes: readonly [style]; keyframes: readonly [keyframes]; resolvers: readonly [selectors.*, @media.*, @supports.*, base, variants.*.*, compoundVariants.*.style] } } }
      • readonlyplugins: { logical-properties: { meta: { name: string; version: string }; rules: { border: RuleModule; border-radius: RuleModule; clear: RuleModule; float: RuleModule; inset: RuleModule; margin: RuleModule; overflow: RuleModule; overscroll-behavior: RuleModule; padding: RuleModule; size: RuleModule } } }
        • readonlylogical-properties: { meta: { name: string; version: string }; rules: { border: RuleModule; border-radius: RuleModule; clear: RuleModule; float: RuleModule; inset: RuleModule; margin: RuleModule; overflow: RuleModule; overscroll-behavior: RuleModule; padding: RuleModule; size: RuleModule } }
          • readonlymeta: { name: string; version: string }
            • readonlyname: string
            • readonlyversion: string
          • rules: { border: RuleModule; border-radius: RuleModule; clear: RuleModule; float: RuleModule; inset: RuleModule; margin: RuleModule; overflow: RuleModule; overscroll-behavior: RuleModule; padding: RuleModule; size: RuleModule }
            • readonlyborder: RuleModule
            • readonlyborder-radius: RuleModule
            • readonlyclear: RuleModule
            • readonlyfloat: RuleModule
            • readonlyinset: RuleModule
            • readonlymargin: RuleModule
            • readonlyoverflow: RuleModule
            • readonlyoverscroll-behavior: RuleModule
            • readonlypadding: RuleModule
            • readonlysize: RuleModule
      • rules: {}
        • [k string]: T
      • readonlysettings: { logical-properties: { functions: readonly [style, globalStyle, recipe, sprinkles, defineProperties]; jsxAttributes: readonly [style]; keyframes: readonly [keyframes]; resolvers: readonly [selectors.*, @media.*, @supports.*, base, variants.*.*, compoundVariants.*.style] } }
        • readonlylogical-properties: { functions: readonly [style, globalStyle, recipe, sprinkles, defineProperties]; jsxAttributes: readonly [style]; keyframes: readonly [keyframes]; resolvers: readonly [selectors.*, @media.*, @supports.*, base, variants.*.*, compoundVariants.*.style] }
          • readonlyfunctions: readonly [style, globalStyle, recipe, sprinkles, defineProperties]
          • readonlyjsxAttributes: readonly [style]
          • readonlykeyframes: readonly [keyframes]
          • readonlyresolvers: readonly [selectors.*, @media.*, @supports.*, base, variants.*.*, compoundVariants.*.style]
    • recommended: { plugins: { logical-properties: { meta: { name: string; version: string }; rules: { border: RuleModule; border-radius: RuleModule; clear: RuleModule; float: RuleModule; inset: RuleModule; margin: RuleModule; overflow: RuleModule; overscroll-behavior: RuleModule; padding: RuleModule; size: RuleModule } } }; rules: {}; settings: { logical-properties: { functions: readonly [style, globalStyle, recipe, sprinkles, defineProperties]; jsxAttributes: readonly [style]; keyframes: readonly [keyframes]; resolvers: readonly [selectors.*, @media.*, @supports.*, base, variants.*.*, compoundVariants.*.style] } } }
      • readonlyplugins: { logical-properties: { meta: { name: string; version: string }; rules: { border: RuleModule; border-radius: RuleModule; clear: RuleModule; float: RuleModule; inset: RuleModule; margin: RuleModule; overflow: RuleModule; overscroll-behavior: RuleModule; padding: RuleModule; size: RuleModule } } }
        • readonlylogical-properties: { meta: { name: string; version: string }; rules: { border: RuleModule; border-radius: RuleModule; clear: RuleModule; float: RuleModule; inset: RuleModule; margin: RuleModule; overflow: RuleModule; overscroll-behavior: RuleModule; padding: RuleModule; size: RuleModule } }
          • readonlymeta: { name: string; version: string }
            • readonlyname: string
            • readonlyversion: string
          • rules: { border: RuleModule; border-radius: RuleModule; clear: RuleModule; float: RuleModule; inset: RuleModule; margin: RuleModule; overflow: RuleModule; overscroll-behavior: RuleModule; padding: RuleModule; size: RuleModule }
            • readonlyborder: RuleModule
            • readonlyborder-radius: RuleModule
            • readonlyclear: RuleModule
            • readonlyfloat: RuleModule
            • readonlyinset: RuleModule
            • readonlymargin: RuleModule
            • readonlyoverflow: RuleModule
            • readonlyoverscroll-behavior: RuleModule
            • readonlypadding: RuleModule
            • readonlysize: RuleModule
      • rules: {}
        • [k string]: T
      • readonlysettings: { logical-properties: { functions: readonly [style, globalStyle, recipe, sprinkles, defineProperties]; jsxAttributes: readonly [style]; keyframes: readonly [keyframes]; resolvers: readonly [selectors.*, @media.*, @supports.*, base, variants.*.*, compoundVariants.*.style] } }
        • readonlylogical-properties: { functions: readonly [style, globalStyle, recipe, sprinkles, defineProperties]; jsxAttributes: readonly [style]; keyframes: readonly [keyframes]; resolvers: readonly [selectors.*, @media.*, @supports.*, base, variants.*.*, compoundVariants.*.style] }
          • readonlyfunctions: readonly [style, globalStyle, recipe, sprinkles, defineProperties]
          • readonlyjsxAttributes: readonly [style]
          • readonlykeyframes: readonly [keyframes]
          • readonlyresolvers: readonly [selectors.*, @media.*, @supports.*, base, variants.*.*, compoundVariants.*.style]
    • warning: { plugins: { logical-properties: { meta: { name: string; version: string }; rules: { border: RuleModule; border-radius: RuleModule; clear: RuleModule; float: RuleModule; inset: RuleModule; margin: RuleModule; overflow: RuleModule; overscroll-behavior: RuleModule; padding: RuleModule; size: RuleModule } } }; rules: {}; settings: { logical-properties: { functions: readonly [style, globalStyle, recipe, sprinkles, defineProperties]; jsxAttributes: readonly [style]; keyframes: readonly [keyframes]; resolvers: readonly [selectors.*, @media.*, @supports.*, base, variants.*.*, compoundVariants.*.style] } } }
      • readonlyplugins: { logical-properties: { meta: { name: string; version: string }; rules: { border: RuleModule; border-radius: RuleModule; clear: RuleModule; float: RuleModule; inset: RuleModule; margin: RuleModule; overflow: RuleModule; overscroll-behavior: RuleModule; padding: RuleModule; size: RuleModule } } }
        • readonlylogical-properties: { meta: { name: string; version: string }; rules: { border: RuleModule; border-radius: RuleModule; clear: RuleModule; float: RuleModule; inset: RuleModule; margin: RuleModule; overflow: RuleModule; overscroll-behavior: RuleModule; padding: RuleModule; size: RuleModule } }
          • readonlymeta: { name: string; version: string }
            • readonlyname: string
            • readonlyversion: string
          • rules: { border: RuleModule; border-radius: RuleModule; clear: RuleModule; float: RuleModule; inset: RuleModule; margin: RuleModule; overflow: RuleModule; overscroll-behavior: RuleModule; padding: RuleModule; size: RuleModule }
            • readonlyborder: RuleModule
            • readonlyborder-radius: RuleModule
            • readonlyclear: RuleModule
            • readonlyfloat: RuleModule
            • readonlyinset: RuleModule
            • readonlymargin: RuleModule
            • readonlyoverflow: RuleModule
            • readonlyoverscroll-behavior: RuleModule
            • readonlypadding: RuleModule
            • readonlysize: RuleModule
      • rules: {}
        • [k string]: T
      • readonlysettings: { logical-properties: { functions: readonly [style, globalStyle, recipe, sprinkles, defineProperties]; jsxAttributes: readonly [style]; keyframes: readonly [keyframes]; resolvers: readonly [selectors.*, @media.*, @supports.*, base, variants.*.*, compoundVariants.*.style] } }
        • readonlylogical-properties: { functions: readonly [style, globalStyle, recipe, sprinkles, defineProperties]; jsxAttributes: readonly [style]; keyframes: readonly [keyframes]; resolvers: readonly [selectors.*, @media.*, @supports.*, base, variants.*.*, compoundVariants.*.style] }
          • readonlyfunctions: readonly [style, globalStyle, recipe, sprinkles, defineProperties]
          • readonlyjsxAttributes: readonly [style]
          • readonlykeyframes: readonly [keyframes]
          • readonlyresolvers: readonly [selectors.*, @media.*, @supports.*, base, variants.*.*, compoundVariants.*.style]
  • readonlymeta: { name: string; version: string }
    • readonlyname: string
    • readonlyversion: string
  • rules: { border: RuleModule; border-radius: RuleModule; clear: RuleModule; float: RuleModule; inset: RuleModule; margin: RuleModule; overflow: RuleModule; overscroll-behavior: RuleModule; padding: RuleModule; size: RuleModule }
    • readonlyborder: RuleModule
    • readonlyborder-radius: RuleModule
    • readonlyclear: RuleModule
    • readonlyfloat: RuleModule
    • readonlyinset: RuleModule
    • readonlymargin: RuleModule
    • readonlyoverflow: RuleModule
    • readonlyoverscroll-behavior: RuleModule
    • readonlypadding: RuleModule
    • readonlysize: RuleModule