Meet Modi
Back to Blog
·5 min·updated

The component library outranked my dir attribute

Toggling content back to English in an Arabic app left it mirrored. Ant Design takes direction from ConfigProvider, and its class outranks any inherited dir.

By Meet Modi
RTLCSSReactInternationalizationDebugging

Ant Design components ignore a dir attribute on an ancestor because antd does not read the DOM for direction. It reads ConfigProvider, and in RTL mode it stamps an ant-*-rtl class on each component whose rule sets direction: rtl on the element itself. A declaration on the element beats a value inherited from an ancestor, so no parent attribute can override it. You have to nest a second ConfigProvider.

The symptom

A learning feed lets you read cards in one language while the app chrome is in another. Switch the cards to English inside an Arabic app and they stayed resolutely right-to-left. Text pushed to the right edge, punctuation on the wrong end, the works.

I had already put a dir attribute on the card for exactly this case. I could see it in the inspector, on the right element, with the right value. The text under it was still mirrored.

Why the attribute was inert

The CSS direction property is inherited. An element with no direction declaration of its own takes its parent's computed value, and the dir attribute is how you set that at the top. That is the mechanism everyone has in mind when they put dir on a wrapper and expect the subtree to follow.

Inheritance only applies when the element has no declaration of its own. In an RTL app, antd gives nearly every component a class like ant-typography-rtl, and that class's rule sets direction: rtl directly on the element. A direct declaration always wins over an inherited value. It is not even a specificity contest, it is a step earlier than that: the element has its own value, so there is nothing to inherit.

Nearly all the text on those cards is antd Typography. So nearly all of it kept the class, kept the declaration, and kept facing the wrong way.

Confirming it instead of reasoning about it

This is the kind of cascade claim that is easy to get confidently wrong, so I checked it against a real build rather than arguing from the spec.

// Inside a dir="ltr" ancestor, in an otherwise RTL app:
getComputedStyle(plainSpan).direction          // "ltr", inherited, fine
getComputedStyle(antTypographySpan).direction  // "rtl", its own declaration

// Drop just the class, which is what a nested ConfigProvider does:
antTypographySpan.classList.remove("ant-typography-rtl")
getComputedStyle(antTypographySpan).direction  // "ltr"

Three lines in a console on the QA build, and the theory stopped being a theory.

The fix: say it twice, once per system

Two independent systems own direction on this subtree, so the boundary has to state it for both. ConfigProvider for antd, and the dir attribute for plain markup and for Tailwind's logical utilities, which read the attribute and know nothing about antd.

// Renders as `contents`, so the feed's children stay siblings of
// whatever they were before this wrapper existed.
export function ContentLanguageScope({ dir, children }) {
  return (
    <ConfigProvider direction={dir}>
      <div dir={dir} style={{ display: "contents" }}>
        {children}
      </div>
    </ConfigProvider>
  );
}

The general shape

Any component library with its own theming context has this property. The library is not reading your DOM. It is reading its provider, and then writing concrete declarations onto elements. A DOM attribute and a provider value that disagree will not negotiate, and the one that wrote a declaration onto the element wins.

Which means direction is not one setting. It is one setting per system that thinks it owns direction, and you need all of them to agree at the same boundary. Same lesson as the row that split in half, arriving from the other direction.

More Posts