CSS logical properties: the full map from left and width to inline and block

margin-left always means the left of the screen. margin-inline-start means wherever a line of text begins, so the same rule is right for English, Arabic and vertical Japanese.

CSS logical properties name the sides of a box by the flow of text instead of the screen. margin-inline-start is the side where a line begins: the left in English, the right in Arabic, the top in vertical Japanese.

inline-size is the size along the line, and block-size is the size across lines.

Pick a property below. Each tile applies it for real, then asks the browser which physical property received the value.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Logical property mapping explorer</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  label { font-weight: 600; font-size: 14px; }
  select { font: inherit; font-size: 15px; padding: 6px 8px; margin-block-start: 6px; inline-size: 100%; max-inline-size: 320px; }
  .grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-block-start: 12px; }
  .tile { background: #fff; border-radius: 10px; padding: 10px; box-shadow: 0 2px 8px rgba(0, 0, 0, .08); }
  .tile h3 { margin: 0 0 8px; font-size: 13px; font-weight: 600; color: #4b5563; }
  /* The stage is a fixed physical frame on purpose: it is the "screen" */
  .stage { position: relative; height: 130px; border: 1px dashed #b8c0cc; border-radius: 6px; overflow: hidden; }
  .box { background: #dbeafe; padding: 6px; font-size: 15px; line-height: 1.4; }
  .box.pad { background: #bbf7d0; }        /* padding shows as green */
  .box.pad span { display: block; background: #dbeafe; }
  .out { margin-block-start: 8px; font: 600 13px ui-monospace, Consolas, monospace; color: #0f5132; }
  .probe { position: absolute; visibility: hidden; }
</style>
</head>
<body>
<label for="prop">Logical property</label><br>
<select id="prop">
  <option>margin-inline-start</option>
  <option>margin-inline-end</option>
  <option>margin-block-start</option>
  <option>margin-block-end</option>
  <option>padding-inline-start</option>
  <option>padding-block-end</option>
  <option selected>border-inline-start</option>
  <option>border-block-start</option>
  <option>inset-inline-start</option>
  <option>inset-block-end</option>
  <option>inline-size</option>
  <option>block-size</option>
</select>

<div class="grid" id="grid"></div>

<script>
  // Four writing setups: the same CSS lands on a different physical side in each
  const modes = [
    { name: 'horizontal-tb, ltr', dir: 'ltr', wm: 'horizontal-tb', text: 'Text starts here' },
    { name: 'horizontal-tb, rtl', dir: 'rtl', wm: 'horizontal-tb', text: 'يبدأ النص هنا' },
    { name: 'vertical-rl', dir: 'ltr', wm: 'vertical-rl', text: '縦書きの文' },
    { name: 'vertical-lr', dir: 'ltr', wm: 'vertical-lr', text: '縦書きの文' },
  ];
  const grid = document.getElementById('grid');
  const select = document.getElementById('prop');

  modes.forEach((m) => {
    grid.insertAdjacentHTML('beforeend',
      `<div class="tile"><h3>${m.name}</h3>
        <div class="stage" dir="${m.dir}" style="writing-mode:${m.wm}">
          <div class="box"><span>${m.text}</span></div>
          <div class="probe">x</div>
        </div>
        <div class="out"></div></div>`);
  });

  // Ask the browser which physical property received the value
  function physicalName(probe, prop) {
    probe.style.cssText = '';
    if (prop === 'inline-size' || prop === 'block-size') {
      probe.style.setProperty(prop, '123px');
      return getComputedStyle(probe).width === '123px' ? 'width' : 'height';
    }
    const family = prop.split('-')[0];              // margin, padding, border, inset
    probe.style.setProperty(prop, family === 'border' ? '7px solid' : '7px');
    if (family === 'inset') probe.style.position = 'relative';
    const cs = getComputedStyle(probe);
    for (const side of ['top', 'right', 'bottom', 'left']) {
      const name = family === 'inset' ? side
        : family === 'border' ? `border-${side}-width` : `${family}-${side}`;
      if (cs.getPropertyValue(name) === '7px') return family === 'border' ? `border-${side}` : name;
    }
    return '?';
  }

  // Apply the property for real so you can see where it lands
  function show(box, prop) {
    box.style.cssText = '';
    box.className = 'box';
    const family = prop.split('-')[0];
    if (family === 'margin') box.style.setProperty(prop, '28px');
    if (family === 'padding') { box.classList.add('pad'); box.style.setProperty(prop, '28px'); }
    if (family === 'border') box.style.setProperty(prop, '6px solid #16a34a');
    if (family === 'inset') { box.style.position = 'absolute'; box.style.setProperty(prop, '0'); }
    if (prop === 'inline-size' || prop === 'block-size') box.style.setProperty(prop, '80px');
  }

  function update() {
    const prop = select.value;
    grid.querySelectorAll('.tile').forEach((tile) => {
      show(tile.querySelector('.box'), prop);
      tile.querySelector('.out').textContent = '= ' + physicalName(tile.querySelector('.probe'), prop);
    });
  }
  select.addEventListener('change', update);
  update();
</script>
</body>
</html>
One logical property, four writing setups. The green text under each tile is the physical property the browser mapped it to.

The basics of right-to-left pages, including <bdi> and which icons to mirror, are in HTML dir="rtl". This guide is the property map itself and the traps in it.

Two axes: inline and block

Every logical name is built from two axes. The inline axis runs along a line of text. The block axis runs the way lines stack, like paragraphs down a page.

In horizontal text the inline axis is left-right and the block axis is down. In vertical-rl the inline axis runs down and lines stack right to left.
In horizontal text the inline axis is left-right and the block axis is down. In vertical-rl the inline axis runs down and lines stack right to left.

Each axis has a start and an end. Two things decide where they are:

  • dir (or the CSS direction property) flips inline start and end: in rtl, start is on the right.
  • writing-mode turns the axes: in vertical-rl, inline runs top to bottom and block runs right to left. CSS writing-mode covers the modes themselves.

The full physical-to-logical map

Every physical side and size has a logical partner. The name tells you the axis and the end.

Physical (in ltr, horizontal) Logical
width / height inline-size / block-size
min-width / max-width min-inline-size / max-inline-size
min-height / max-height min-block-size / max-block-size
margin-left / margin-right margin-inline-start / margin-inline-end
margin-top / margin-bottom margin-block-start / margin-block-end
padding-left / padding-top padding-inline-start / padding-block-start
border-left / border-bottom border-inline-start / border-block-end
left / right inset-inline-start / inset-inline-end
top / bottom inset-block-start / inset-block-end
border-top-left-radius border-start-start-radius
border-bottom-right-radius border-end-end-radius
text-align: left / right text-align: start / end

The pattern repeats for every family: margin, padding, border, border-width, border-color, border-style and inset each have -inline-start, -inline-end, -block-start and -block-end versions.

Corner names put the block side first, then the inline side. border-start-end-radius is the corner where block-start meets inline-end: top-right in an English page.

For text-align, start is already the default, so ordinary text needs no rule at all. CSS text-align goes further into start, end and justify.

Where each one lands

Here is the same map read the other way: which physical side a logical property becomes in each setup. The explorer above measures these live.

Logical ltr rtl vertical-rl vertical-lr
margin-inline-start left right top top
margin-inline-end right left bottom bottom
margin-block-start top top right left
margin-block-end bottom bottom left right
inline-size width width height height
block-size height height width width
border-start-start-radius top-left top-right top-right top-left

Two things to notice. dir="rtl" only changes the inline row; block-start stays on top. And in vertical text block-size is the width, so min-block-size sets a minimum width there.

Shorthands read start, then end

The two-value logical shorthands are an easy trap. margin-inline: 8px 24px looks like margin: 8px 24px, but it means something else.

margin-inline takes start then end, both on the inline axis. It never sets top or bottom.
margin-inline takes start then end, both on the inline axis. It never sets top or bottom.
.a { margin-inline: 8px 24px; } /* start 8px, end 24px */
.b { margin-inline: 16px; }     /* start and end both 16px */
.c { padding-block: 12px 0; }   /* block-start 12px, block-end 0 */
.d { margin-inline: auto; }     /* centers a block along the line */

The same rule applies to padding-inline, padding-block, margin-block, inset-inline, inset-block, border-inline-width and friends.

Watch out for inset on its own. Despite the name, it is a physical shorthand for top right bottom left. It does not flip with dir.

Converting a component

Converting existing CSS is a search-and-replace with a rule for each family. Test with each language button below: only dir and writing-mode change, the CSS stays the same.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Physical vs logical card</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .bar { display: flex; gap: 6px; flex-wrap: wrap; }
  .bar button { font: inherit; font-size: 14px; padding: 7px 12px; border: 1px solid #c7ced8; border-radius: 8px; background: #fff; cursor: pointer; }
  .bar button[aria-pressed="true"] { background: #1d2330; color: #fff; border-color: #1d2330; }
  .panels { display: flex; flex-wrap: wrap; gap: 14px; margin-top: 12px; }
  .panel { flex: 1 1 260px; }
  .panel h3 { margin: 0 0 8px; font-size: 13px; color: #4b5563; }
  .panel h3 b { color: #9a3412; }
  .panel.logical h3 b { color: #0f5132; }

  /* Shared look: flexbox already follows the text direction */
  .card { box-sizing: border-box; position: relative; display: flex; align-items: flex-start; background: #fff; box-shadow: 0 2px 10px rgba(0, 0, 0, .1); }
  .avatar { flex: none; width: 40px; height: 40px; border-radius: 50%; background: linear-gradient(135deg, #60a5fa, #2563eb); }
  .name { font-weight: 700; }
  .text { font-size: 14px; color: #4b5563; margin-top: 4px; }
  .badge { position: absolute; font-size: 11px; font-weight: 700; background: #fde68a; padding: 2px 7px; border-radius: 99px; }

  /* 1. Physical: every value names a side of the screen */
  .physical .card { width: 220px; padding: 14px 44px 14px 14px; border-left: 5px solid #f97316;
                    border-radius: 0 12px 12px 0; text-align: left; }
  .physical .avatar { margin-right: 12px; }
  .physical .badge { top: 10px; right: 10px; }

  /* 2. Logical: every value names a side of the text flow */
  .logical .card { inline-size: 220px; padding-block: 14px; padding-inline: 14px 44px; border-inline-start: 5px solid #16a34a;
                   border-start-end-radius: 12px; border-end-end-radius: 12px; text-align: start; }
  .logical .avatar { margin-inline-end: 12px; }
  .logical .badge { inset-block-start: 10px; inset-inline-end: 10px; }
</style>
</head>
<body>
<div class="bar" id="bar">
  <button data-lang="en" aria-pressed="true">English</button>
  <button data-lang="ar">Arabic (rtl)</button>
  <button data-lang="ja">Japanese (vertical)</button>
</div>

<div class="panels">
  <div class="panel physical"><h3><b>Physical:</b> width, margin-right, border-left, right</h3><div class="slot"></div></div>
  <div class="panel logical"><h3><b>Logical:</b> inline-size, margin-inline-end, border-inline-start, inset-inline-end</h3><div class="slot"></div></div>
</div>

<script>
  const content = {
    en: { dir: 'ltr', wm: 'horizontal-tb', name: 'Sam Lee', text: 'Sent you a new file.', badge: 'New' },
    ar: { dir: 'rtl', wm: 'horizontal-tb', name: 'سارة', text: 'أرسلت لك ملفا جديدا.', badge: 'جديد' },
    ja: { dir: 'ltr', wm: 'vertical-rl', name: '佐藤', text: 'ファイルを送りました。', badge: '新着' },
  };

  function render(lang) {
    const c = content[lang];
    document.querySelectorAll('.slot').forEach((slot) => {
      // Only dir and writing-mode change. The card CSS stays the same.
      slot.innerHTML =
        `<div class="card" dir="${c.dir}" lang="${lang}" style="writing-mode:${c.wm}">
           <div class="avatar"></div>
           <div><div class="name">${c.name}</div><div class="text">${c.text}</div></div>
           <span class="badge">${c.badge}</span>
         </div>`;
    });
    document.querySelectorAll('#bar button').forEach((b) =>
      b.setAttribute('aria-pressed', String(b.dataset.lang === lang)));
  }

  document.getElementById('bar').addEventListener('click', (e) => {
    if (e.target.dataset.lang) render(e.target.dataset.lang);
  });
  render('en');
</script>
</body>
</html>
The same card twice. Arabic puts the orange accent on the wrong side of the physical card; Japanese keeps its physical width. The logical card follows both.
  1. Sizes: width and height become inline-size and block-size, including the min- and max- versions.
  2. Left and right: margin-left, padding-left and border-left become the inline-start versions. The right ones become inline-end.
  3. Top and bottom: these become block-start and block-end.
  4. Positions and corners: left and right become inset-inline-start and inset-inline-end. border-top-left-radius becomes border-start-start-radius.
  5. Alignment: text-align: left becomes start.
  6. Test: set dir="rtl", then writing-mode: vertical-rl, and check what moves.

The card's flex layout needed no change. Flexbox and grid already follow the inline direction, so only the margins, borders and positions inside them had to switch.

Logical properties are still part of the same box model: margin, border, padding and content. The CSS box model explains how those layers add up.

When physical properties are still right

Not everything should flip. Logical properties are for things placed relative to the words. Things tied to the screen, to light or to a picture should stay put.

Left: anything positioned relative to text. Right: shadows, pictures and screen frames, which keep physical values.
Left: anything positioned relative to text. Right: shadows, pictures and screen frames, which keep physical values.
  • Shadows: box-shadow offsets only take physical x and y values. A light source above and to the left does not move when the language changes.
  • Pictures: a gradient or background that draws a scene, such as a sunset, keeps to right.
  • Screen frames: a box that stands for a device screen keeps width and height. The frame in the chat example below does exactly that.

A finished example: chat and sidebar

This layout uses only logical properties for the sidebar, the bubbles, the timestamps and the input. The buttons switch the app between English, Arabic and a vertical Japanese preview.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Chat and sidebar with logical properties</title>
<style>
  body { margin: 0; padding: 12px; font-family: system-ui, sans-serif; background: #eef1f5; color: #1d2330; }
  .bar { display: flex; gap: 6px; flex-wrap: wrap; margin-block-end: 10px; }
  .bar button { font: inherit; font-size: 14px; padding: 7px 12px; border: 1px solid #c7ced8; border-radius: 8px; background: #fff; cursor: pointer; }
  .bar button[aria-pressed="true"] { background: #1d2330; color: #fff; border-color: #1d2330; }

  /* The frame is the "screen", so it keeps physical width and height */
  .frame { width: 100%; height: 400px; }

  /* Everything below uses logical properties only */
  .app { inline-size: 100%; block-size: 100%; display: grid; grid-template-columns: 112px 1fr;
         background: #fff; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 16px rgba(0, 0, 0, .1); }
  .side { background: #1d2330; color: #cbd5e1; padding-block: 12px; padding-inline: 10px; }
  .side a { display: block; padding-block: 8px; padding-inline: 10px; margin-block-end: 4px; border-radius: 8px;
            color: inherit; text-decoration: none; font-size: 14px; }
  .side a.on { background: #334155; color: #fff; border-inline-start: 3px solid #4ade80; }
  .chat { display: flex; flex-direction: column; min-inline-size: 0; }
  .head { padding-block: 10px; padding-inline: 14px; border-block-end: 1px solid #e5e7eb; font-weight: 700; }
  .list { flex: 1; overflow: auto; padding: 12px; display: flex; flex-direction: column; gap: 8px; }
  .msg { max-inline-size: 75%; padding-block: 8px; padding-inline: 12px; border-radius: 14px; line-height: 1.45; }
  .them { background: #f1f5f9; border-end-start-radius: 4px; }                     /* tail at start */
  .me { background: #2563eb; color: #fff; margin-inline-start: auto; border-end-end-radius: 4px; } /* pushed to end */
  .time { display: block; font-size: 11px; opacity: .7; text-align: end; margin-block-start: 2px; }
  .send { display: flex; gap: 8px; padding: 10px; border-block-start: 1px solid #e5e7eb; }
  .send input { flex: 1; min-inline-size: 0; font: inherit; padding-block: 8px; padding-inline: 12px; border: 1px solid #cbd5e1; border-radius: 99px; }
  .send button { font: inherit; padding-inline: 14px; border: 0; border-radius: 99px; background: #2563eb; color: #fff; }
</style>
</head>
<body>
<div class="bar" id="bar">
  <button data-lang="en" aria-pressed="true">English</button>
  <button data-lang="ar">العربية (rtl)</button>
  <button data-lang="ja">日本語 (vertical-rl)</button>
</div>

<div class="frame">
  <div class="app" id="app">
    <nav class="side" id="side"></nav>
    <section class="chat">
      <div class="head" id="head"></div>
      <div class="list" id="list"></div>
      <form class="send" id="form"><input id="input" autocomplete="off"><button id="btn"></button></form>
    </section>
  </div>
</div>

<script>
  const ui = {
    en: { dir: 'ltr', wm: 'horizontal-tb', nav: ['Chats', 'People', 'Settings'], head: 'Sam Lee', send: 'Send', hint: 'Message',
          msgs: [['them', 'Hi! Did the file arrive?'], ['me', 'Yes, got it. Thanks!'], ['them', 'Great, see you at 3.']] },
    ar: { dir: 'rtl', wm: 'horizontal-tb', nav: ['المحادثات', 'الأشخاص', 'الإعدادات'], head: 'سارة', send: 'إرسال', hint: 'رسالة',
          msgs: [['them', 'مرحبا! هل وصل الملف؟'], ['me', 'نعم، وصل. شكرا!'], ['them', 'رائع، أراك الساعة 3.']] },
    ja: { dir: 'ltr', wm: 'vertical-rl', nav: ['トーク', '連絡先', '設定'], head: '佐藤', send: '送信', hint: 'メッセージ',
          msgs: [['them', 'ファイルは届きましたか?'], ['me', 'はい、届きました。'], ['them', 'では三時に。']] },
  };
  const app = document.getElementById('app');
  const list = document.getElementById('list');
  let lang = 'en';

  function bubble(who, text) {
    const div = document.createElement('div');
    div.className = 'msg ' + who;
    div.textContent = text;
    div.insertAdjacentHTML('beforeend', '<span class="time">9:41</span>');
    list.append(div);
  }

  function render(next) {
    lang = next;
    const t = ui[lang];
    // The only switches: dir, lang and writing-mode on the app
    app.dir = t.dir; app.lang = lang; app.style.writingMode = t.wm;
    document.getElementById('side').innerHTML = t.nav.map((n, i) => `<a href="#" class="${i ? '' : 'on'}">${n}</a>`).join('');
    document.getElementById('head').textContent = t.head;
    document.getElementById('btn').textContent = t.send;
    document.getElementById('input').placeholder = t.hint;
    list.innerHTML = '';
    t.msgs.forEach(([who, text]) => bubble(who, text));
    document.querySelectorAll('#bar button').forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.lang === lang)));
  }

  document.getElementById('bar').addEventListener('click', (e) => { if (e.target.dataset.lang) render(e.target.dataset.lang); });

  // Sending adds your message at the end side, in any direction
  document.getElementById('form').addEventListener('submit', (e) => {
    e.preventDefault();
    const input = document.getElementById('input');
    if (input.value.trim()) { bubble('me', input.value.trim()); input.value = ''; list.lastElementChild.scrollIntoView({ block: 'nearest', inline: 'nearest' }); }
  });
  render('en');
</script>
</body>
</html>
Type a message and press Send. Your bubble always lands at the inline end, whatever the direction.
  • Your bubbles: margin-inline-start: auto pushes them to the end of the line.
  • Bubble tails: border-end-end-radius and border-end-start-radius keep the small corner next to the sender.
  • Sidebar: padding-inline and border-inline-start on the active link. Grid places the sidebar at the inline start.
  • Width limits: max-inline-size: 75% on bubbles and min-inline-size: 0 on the input so it can shrink.

When it does not work

What you see Cause Fix
A margin or border stays on the left in rtl A physical property is still in the CSS Search for left and right and swap them
One side gets both values Two-value shorthand read as physical margin-inline: a b is start then end
inset does not flip inset is physical Use inset-inline and inset-block
inline-size does nothing The element is display: inline Use inline-block or block
A box is too wide in vertical text block-size is the width there Set the column length with inline-size
A shorthand is ignored in an old browser Some shorthands shipped later than longhands Use the longhands, or check a support table

The last row is about age, not correctness. The longhand properties, such as margin-inline-start, arrived in browsers before some of the shorthands and the inset-* family. If you need to support older browsers, check each property on MDN.

Direction bugs only show up when someone reads the page in the other direction. A screenshot shows one direction; a working page lets a reviewer press the Arabic or Japanese button and check for themselves.

To send the working version, paste the page into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written and its scripts run, so the language switches work for whoever opens it. If you change the CSS later, the same link shows the new version.

Questions people ask

Is there an "inline height" in CSS?

No property has that name. In horizontal text, the height of a box is block-size and its width is inline-size. In vertical text the two swap: inline-size becomes the height and block-size becomes the width.

What is the difference between inline-size and width?

width is always horizontal. inline-size is the size along the line of text, so it is the width in horizontal writing and the height in vertical writing. In an ordinary left-to-right or right-to-left page the two give the same result.

What does margin-inline: 10px 20px mean?

The first value is margin-inline-start and the second is margin-inline-end. It does not touch the top or bottom margins. With one value, both start and end get it. padding-inline, margin-block and padding-block follow the same pattern.

Is inset a logical property?

No. inset is a shorthand for top, right, bottom and left, in that physical order. The logical versions are inset-inline, inset-block and the four longhands such as inset-inline-start.

Should I replace every physical property with a logical one?

Replace the ones that describe where something sits relative to text: margins, padding, borders, positions and alignment around content. Keep physical values for things tied to the screen, such as box-shadow offsets, gradients that paint a picture, and frames that stand for a device.

Keep reading