prefer-stack-over-flex

Github

Prefer Stack and InlineStack over the lower-level Flex component.


Flex exposes the raw flexbox model, so every layout has to restate its direction and then name its alignment in terms of the main and cross axes — which swap meaning when the direction changes. Stack and InlineStack fix the direction and name the axes after the writing mode instead, so alignInline and alignBlock mean the same thing everywhere.

Most usages fix themselves.

Incorrect

import { Flex } from '@customerio/pluma-components/react';

<Flex direction="column" gap="200">…</Flex>
<Flex justifyContent="space-between" alignItems="center">…</Flex>

Correct

import { InlineStack, Stack } from '@customerio/pluma-components/react';

<Stack alignInline="start" gap="200">…</Stack>
<InlineStack alignInline="space-between" alignBlock="center">…</InlineStack>

What the autofix does

The fix picks the component from the static direction, drops that prop, renames the alignment props onto the axis they map to, and adds the replacement to the Pluma import in the same pass.

table
Flexdirection="column" → Stackdirection="row" (or none) → InlineStack
alignItemsalignInlinealignBlock
justifyContentalignBlockalignInline

A converted column also gains an explicit alignInline="start" when it had no alignItems, because Flex defaults its cross axis to start while Stack defaults alignInline to stretch. Without it the fix would quietly change the layout.

The Flex import is left in place even when every element converts, so a remaining styled(Flex) or ComponentProps<typeof Flex> keeps working. Delete the import once nothing references it. If the replacement is already imported — including under an alias, such as InlineStack as Row — the fix reuses that local name instead of adding a second binding.

What is reported without a fix

Some usages are reported for a hand-written change instead, and the Flex import is left in place for them:

  • A direction the rule cannot read statically, or a reversed direction (row-reverse, column-reverse), which neither component supports. The report names both components and leaves the choice to you.
  • wrap on a column, which Stack does not support. Wrapping a column usually means the layout wants Grid.
  • flexWrap on a column. Flex overwrites it with its own wrap, so it is inert today; Stack never sets it, so converting would make it live.
  • flexDirection, which no-ineffective-stack-props covers: both components fix their own direction and ignore it.
  • Spread props, which may carry direction, justifyContent, or alignItems. In Ember that includes @unsafe_props, since propsProxy falls back to it for any argument the element does not pass explicitly.
  • An alignment prop whose target name is already taken.
  • A Glimmer element whose closing tag cannot be located, such as </PlumaFlex >. Renaming only the opening tag would emit mismatched tags.
  • A replacement name already bound elsewhere in the file, which the fix cannot introduce without clashing.
  • A per-component subpath import such as @customerio/pluma-components/react/flex, which does not export the replacement. Import Stack or InlineStack yourself and the usage converts on the next run.
  • An alignment value the rule cannot read statically, on a column. Flex drops an undefined prop and falls back to start; Stack would fall back to stretch.
  • alignBlock or alignInline written on the Flex itself. They do nothing there, but the replacement would honour them.
  • A prop set more than once. Both frameworks apply the last one, the rule reads the first, so it cannot tell which value is live.
  • A name the fix would introduce or rely on that is bound elsewhere in the file — including by a local variable, a parameter, or a Glimmer block param, any of which would shadow the component at the use site.

Each of these is skipped on its own. Other usages in the same file still convert.

What is checked

The rule resolves Flex and PlumaFlex through the Pluma entry points before matching usage, so a component imported as Flex from anywhere else is ignored. Matching is by tag name, so if something in the file shadows Flex — a local variable, a Glimmer block param — the rule cannot tell the two apart and refuses to fix anything in that file.

An aliased import is reported but not fixed, and neither is a file with more than one Flex import declaration, because the fix has to extend one known import alongside the elements. A type-only import of the replacement does not count as one it can reuse.