RTL Support

v1.1+

For right-to-left (RTL) languages such as Arabic and Hebrew to be semantically appropriate, in addition to translating the texts, you need to also mirror the layout.

Chakra UI makes it possible to support RTL languages and LTR languages in the same app. There are 2 methods of adding RTL support:

  • Using the RTL Stylis Plugin
  • Using RTL-aware style props

Using RTL Stylis Plugin#

Since Chakra UI is built on top of @emotion/react, you can leverage stylis plugins like stylis-plugin-rtl to automatically transform the generated styles to their RTL equivalent.

Here's how to set it up.

  1. Install the stylis plugin and emotion's cache
npm i stylis stylis-plugin-rtl @emotion/cache
# or
yarn add stylis stylis-plugin-rtl @emotion/cache
  1. Create the RTL provider using CacheProvider from emotion
// src/components/rtl-provider.js
import { CacheProvider } from "@emotion/react"
import createCache from "@emotion/cache"
import rtl from "stylis-plugin-rtl"
// NB: A unique `key` is important for it to work!
const options = {
rtl: { key: "css-ar", stylisPlugins: [rtl] },
ltr: { key: "css-en" },
}
export function RtlProvider({ children }) {
const { locale } = useRouter()
const dir = locale == "ar" ? "rtl" : "ltr"
const cache = createCache(options[dir])
return <CacheProvider value={cache} children={children} />
}
  1. Add RTL provider to the application's root
// pages/_app.js
import { ChakraProvider } from "@chakra-ui/react"
import { RtlProvider } from "@/components/rtl-provider"
function App(props) {
const { Component, pageProps } = props
return (
<ChakraProvider>
<RtlProvider>
<Component {...pageProps} />
</RtlProvider>
</ChakraProvider>
)
}
export default App
  1. Add the dir and lang attributes to the html tag.

You'll need to make a few changes to your markup. In the <html> tag, you'll need to add a dir attribute with a value of rtl or ltr. Here's what your <html> tag should look like:

/**
* `lang` can be "ar", "en", "he", etc.
* `dir` can be "rtl" or "ltr"
*/
<html lang="ar" dir="rtl">
{/* Content */}
</html>

In Next.js, you can achieve this by adding a pages/_document.js file and using this API:

// pages/_document.js
import NextDocument, { Html, Head, Main, NextScript } from "next/document"
class Document extends NextDocument {
static async getInitialProps(ctx) {
return await NextDocument.getInitialProps(ctx)
}
render() {
const { locale } = this.props.__NEXT_DATA__
const dir = locale === "ar" ? "rtl" : "ltr"
return (
{/* 👇🏻 Here's the place to change the `dir` and `lang` */}
<Html dir={dir} lang={locale}>
<Head />
<body >
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default Document
  1. Add a way to switch between LTR and RTL

For example, in Next.js, you can change the locale on the route by leveraging the built-in useRouter hook.

// src/components/lang-switcher.js
function LangSwitcher() {
const { locale, push, reload, pathname } = useRouter()
const nextLocale = locale === "en" ? "ar" : "en"
const onClick = async () => {
await push(pathname, { locale: nextLocale })
// force a reload for it to work correctly.
reload()
}
return <button onClick={onClick}>Change to {nextLocale}</button>
}
export default LangSwitcher

Caveats of this approach#

  • You might need to force a reload of the page to get it working correctly.
  • You might need to change the placement of components like Popover, Drawer, Tooltip to match RTL.
  • The need to install extra packages like stylis, stylis-plugin-rtl might increase your final bundle.

Using RTL-aware style props#

This is the recommended way to support RTL in Chakra UI. With this approach we use the appropriate CSS logical properties, and manage the placements of components like Popover, Drawer, Tooltip to match RTL.

Here's how to set it up:

  1. Add a direction key to the theme

You can use the extendTheme function or any other preferred approach to add direction key to the theme. Then, add the custom theme to ChakraProvider.

Due to the fact the some CSS logical properties aren't supported in all browsers, we flip the styles based on the direction as a temporary polyfill.

// src/components/chakra-rtl-provider.js
function ChakraRTLProvider({ children }) {
const { locale } = useRouter()
const direction = locale === "ar" ? "rtl" : "ltr"
// 👇🏻 Here's the place we add direction to the theme
const theme = extendTheme({ direction })
return <ChakraProvider theme={theme}>{children}</ChakraProvider>
}
  1. Add the dir and lang attributes to the html tag.

In Next.js, you can achieve this by adding a pages/_document.js file and using this API:

// pages/_document.js
import NextDocument, { Html, Head, Main, NextScript } from "next/document"
class Document extends NextDocument {
static async getInitialProps(ctx) {
return await NextDocument.getInitialProps(ctx)
}
render() {
const { locale } = this.props.__NEXT_DATA__
const dir = locale === "ar" ? "rtl" : "ltr"
return (
{/* 👇🏻 Here's the place to change the `dir` and `lang` */}
<Html dir={dir} lang={locale}>
<Head />
<body >
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default Document
  1. Replace style props with their RTL-aware equivalent

To get our internal RTL system working, you need to replace all physical *-left or *-right styles (passed as props or in the sx prop) to their bi-directional versions (*-start or *-end).

For example:

  • Replace paddingLeft or pl prop with paddingStart or ps
  • Replace marginRight or mr prop with marginEnd or me
  • Replace borderLeftRadius with borderStartRadius

Here's a list of the RTL-aware style props you can use alongside other CSS logical properties:

Style propReplace withDescription
paddingLeft, plpaddingStart, pspadding in start direction
paddingRight, prpaddingEnd, pepadding in end direction
marginLeft, mlmarginStart, msmargin in start direction
marginRight, mrmarginEnd, memargin in end direction
roundedLeft, borderLeftRadiusroundedStart, borderStartRadiusrounded borders in start direction
roundedRight, borderRightRadiusroundedEnd, borderEndRadiusrounded borders in end direction
roundedTopLeft, borderTopLeftRadiusroundedTopStart, borderTopStartRadiusrounded borders in top start direction
roundedTopRight, borderTopRightRadiusroundedTopEnd, borderTopEndRadiusrounded borders in top end direction
roundedBottomLeft, borderBottomLeftRadiusroundedBottomStart, borderBottomStartRadiusrounded borders in bottom start direction
roundedBottomRight, borderBottomRightRadiusroundedBottomEnd, borderBottomEndRadiusrounded borders in bottom end direction
borderLeftborderStart, borderInlineStartborder width in start direction
borderRightborderEnd, borderInlineEndborder width in end direction
  1. Add a way to switch between LTR and RTL

For example, in Next.js, you can change the locale on the route by leveraging the built-in useRouter hook.

// src/components/lang-switcher.js
function LangSwitcher() {
const { locale, push, reload, pathname } = useRouter()
const nextLocale = locale === "en" ? "ar" : "en"
const onClick = async () => {
await push(pathname, { locale: nextLocale })
}
return <button onClick={onClick}>Change to {nextLocale}</button>
}
export default LangSwitcher

Asides updating the style props you use in your application, we think this is the best approach. In the end it's up to your team to decide which approach to go with.

Additional resources#

If you'd like to share your RTL setup with the community, feel free to add a PR that updates this section.