The HTML ruby tag: small readings above your text

Wrap a word in ruby and put its reading in rt. The browser draws the reading in small type above the word, and rp adds parentheses for places where that layout is missing.

The HTML <ruby> element puts a small reading above a word. Write the word, then its reading in an <rt> element, and wrap both in <ruby>. Browsers draw the rt text in small type over the base text.

That is how pages show furigana over Japanese kanji and pinyin over Chinese characters.

Try it. Turn the readings off, then move them under the text.

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>Ruby readings</title>
<style>
  body {
    margin: 0; padding: 18px; background: #f4f5f7; color: #1d2330;
    /* system fonts that cover Japanese and Chinese */
    font-family: system-ui, "Hiragino Sans", "Yu Gothic", Meiryo,
      "Noto Sans CJK JP", "PingFang SC", "Microsoft YaHei", sans-serif;
  }
  .controls { display: flex; flex-wrap: wrap; gap: 8px 18px; margin-bottom: 14px; font-size: 14px; }
  .card { background: #fff; border-radius: 12px; padding: 12px 16px; margin-bottom: 10px;
          box-shadow: 0 2px 8px rgba(0, 0, 0, .08); }
  .label { font-size: 12px; color: #6b7280; }
  .text { font-size: 30px; line-height: 2.2; }  /* room above and below for the readings */
  rt { font-size: 50%; color: #b4471f; }        /* style the annotation like any text */
  .under ruby { ruby-position: under; }
  .hide rt { visibility: hidden; }              /* keeps the space, so nothing jumps */
</style>
</head>
<body>
<div class="controls">
  <label><input type="checkbox" id="show" checked> Show readings</label>
  <label><input type="radio" name="pos" value="over" checked> Over</label>
  <label><input type="radio" name="pos" value="under"> Under</label>
</div>

<div id="samples">
  <div class="card">
    <div class="label">Japanese furigana, one reading per kanji</div>
    <div class="text" lang="ja">
      <ruby>東<rp>(</rp><rt>とう</rt><rp>)</rp>京<rp>(</rp><rt>きょう</rt><rp>)</rp></ruby>に<ruby>住<rp>(</rp><rt>す</rt><rp>)</rp></ruby>む
    </div>
  </div>
  <div class="card">
    <div class="label">Chinese pinyin</div>
    <div class="text" lang="zh-CN">
      <ruby>你<rp>(</rp><rt>nǐ</rt><rp>)</rp>好<rp>(</rp><rt>hǎo</rt><rp>)</rp></ruby>,<ruby>中<rp>(</rp><rt>zhōng</rt><rp>)</rp>文<rp>(</rp><rt>wén</rt><rp>)</rp></ruby>
    </div>
  </div>
</div>

<script>
  const samples = document.getElementById('samples');

  // show or hide every <rt> with one class on the parent
  document.getElementById('show').addEventListener('change', (e) => {
    samples.classList.toggle('hide', !e.target.checked);
  });

  // move readings above or below the base text
  document.querySelectorAll('input[name="pos"]').forEach((radio) => {
    radio.addEventListener('change', () => {
      samples.classList.toggle('under', radio.value === 'under');
    });
  });
</script>
</body>
</html>
Furigana and pinyin with two controls: hide the readings, and switch ruby-position between over and under.
<ruby>漢<rp>(</rp><rt>かん</rt><rp>)</rp>字<rp>(</rp><rt>じ</rt><rp>)</rp></ruby>

The three elements: ruby, rt and rp

Each element has one job. ruby holds the whole annotated run. rt holds a reading and belongs to the base text just before it. rp holds a bracket that only appears where ruby layout does not.

The markup on the left, and what the browser draws from it on the right.
The markup on the left, and what the browser draws from it on the right.
  1. Put the base text inside <ruby>.
  2. After each base character, or after the whole word, add <rt> with the reading.
  3. Put <rp>(</rp> before each rt and <rp>)</rp> after it.
  4. Add a lang attribute such as lang="ja" or lang="zh-CN" to the text.
  5. Give the paragraph a taller line-height, around 2.

Older tutorials also use <rb> and <rtc>. MDN lists both as deprecated, and the three elements above are all a page needs.

Furigana, pinyin and zhuyin

Furigana are kana readings over kanji. A school text might mark every kanji, while a page for adults marks only rare ones. The markup is the same either way: one rt after the characters it reads.

Pinyin works the same way over Chinese characters, with tone marks typed as ordinary letters such as ǎ or ō. Type them directly in a UTF-8 file. The HTML entities guide covers the cases where a character is hard to type.

Zhuyin (bopomofo) is traditionally set to the right of each character. The CSS Ruby spec defines ruby-position: inter-character for that. The Chromium build we tested did not recognise the value, so check the browsers you target before relying on it.

One reading per character, or one per word

There are two ways to attach readings. Mono ruby puts an rt after every character. Group ruby puts a single rt after the whole word.

Left: one rt per kanji. Right: one rt for a word whose reading cannot be split.
Left: one rt per kanji. Right: one rt for a word whose reading cannot be split.

Mono ruby is clearer for learners, because each reading sits over its own kanji. Some words cannot be split. 今日 (today) reads きょう as a whole, and no part of that reading belongs to 今 alone. Those words take group ruby.

Pattern Markup Use it for
Per character 東<rt>とう</rt>京<rt>きょう</rt> Words whose kanji each have a reading
Whole word 今日<rt>きょう</rt> Readings that belong to the word only
Several words One <ruby> per word Keeping line breaks between words

Styling rt with CSS

rt is ordinary text, so font-size, color and font-weight work on it. Chromium draws it at 50% of the base size by default. Raise it a little for small base text, or lower it for large headings.

.reading { line-height: 2; }             /* room for the readings */
rt { font-size: 50%; color: #b4471f; }
ruby { ruby-position: under; }           /* over is the default */
.quiz rt { visibility: hidden; }         /* hide without moving the text */

Two properties control placement. ruby-position takes over or under and goes on the ruby element or a parent.

ruby-align decides how a reading shorter or longer than its base is spread out. Support for ruby-align varies between browsers, so test it before building on it.

With normal line spacing the readings press into the line above. A line-height of about 2 gives them room.
With normal line spacing the readings press into the line above. A line-height of about 2 gives them room.

Use visibility: hidden rather than display: none to hide readings. The space stays, and the text does not jump when a reading reappears.

What rp is for

Browsers that lay out ruby hide rp, so on a normal page the brackets never show. They matter where ruby layout is missing, such as a text-mode browser or a tool that turns the page into plain text. There the markup flows inline.

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>What rp is for</title>
<style>
  body {
    margin: 0; padding: 16px; background: #f4f5f7; color: #1d2330;
    font-family: system-ui, "Hiragino Sans", "Yu Gothic", Meiryo,
      "Noto Sans CJK JP", "PingFang SC", "Microsoft YaHei", sans-serif;
  }
  .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 10px; }
  .panel { background: #fff; border-radius: 12px; padding: 12px 14px; box-shadow: 0 2px 8px rgba(0, 0, 0, .08); }
  h3 { margin: 0 0 4px; font-size: 14px; }
  p.note { margin: 0; font-size: 12.5px; color: #6b7280; line-height: 1.4; }
  .text { font-size: 24px; line-height: 2; margin: 8px 0 4px; white-space: nowrap; }

  /* Pretend the browser has no ruby layout: everything flows inline.
     rp must be switched on by hand, because ruby-aware browsers hide it. */
  .no-ruby ruby, .no-ruby rt { display: inline; font-size: inherit; }
  .no-ruby rp { display: inline; }
</style>
</head>
<body>
<div class="grid">
  <div class="panel">
    <h3>Ruby works</h3>
    <div class="text" lang="ja"><ruby>漢<rp>(</rp><rt>かん</rt><rp>)</rp>字<rp>(</rp><rt>じ</rt><rp>)</rp></ruby></div>
    <p class="note">The rp parentheses are in the markup, and the browser hides them.</p>
  </div>
  <div class="panel no-ruby">
    <h3>No ruby, with rp</h3>
    <div class="text" lang="ja"><ruby>漢<rp>(</rp><rt>かん</rt><rp>)</rp>字<rp>(</rp><rt>じ</rt><rp>)</rp></ruby></div>
    <p class="note">The same markup, flowed inline. The parentheses keep base and reading apart.</p>
  </div>
  <div class="panel no-ruby">
    <h3>No ruby, no rp</h3>
    <div class="text" lang="ja"><ruby>漢<rt>かん</rt>字<rt>じ</rt></ruby></div>
    <p class="note">Without rp, the reading runs straight into the kanji.</p>
  </div>
</div>
</body>
</html>
The same markup three ways: normal ruby, flowed inline with rp, and flowed inline without rp.

Without rp, the inline fallback reads 漢かん字じ, which is hard to follow. With it, the reader gets 漢(かん)字(じ). Adding rp costs a few characters and does not change the normal layout.

Fonts, lang and copying

A browser can only draw characters that some installed font covers. List system fonts that include Japanese and Chinese, and the device still needs a font for those scripts. Missing coverage shows as empty boxes.

font-family: system-ui, "Hiragino Sans", "Yu Gothic", Meiryo,
  "Noto Sans CJK JP", "PingFang SC", "Microsoft YaHei", sans-serif;

The lang attribute matters as well. Japanese and Chinese share many characters that are drawn differently in each language, and lang tells the browser which form to pick. The HTML font family list covers font stacks in more detail.

Copying is the other surprise. rt is real text, so in Chromium-based browsers, selecting 漢字 with its readings copies 漢かん字. Other browsers may differ. user-select: none on rt kept the reading out of the copy in Chromium.

A finished example: vocabulary flashcards

The flashcards below use per-character and whole-word ruby. The button switches to a quiz mode that hides every reading. Tap a card to check your answer.

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>Kanji flashcards</title>
<style>
  body {
    margin: 0; padding: 16px; background: #f4f5f7; color: #1d2330;
    font-family: system-ui, "Hiragino Sans", "Yu Gothic", Meiryo,
      "Noto Sans CJK JP", "PingFang SC", "Microsoft YaHei", sans-serif;
  }
  .bar { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 12px; }
  button { font: inherit; font-size: 14px; padding: 8px 14px; border: 0; border-radius: 8px;
           background: #1d2330; color: #fff; cursor: pointer; }
  #score { font-size: 14px; color: #4b5563; }
  .cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px;
           list-style: none; margin: 0; padding: 0; }
  .cards li { background: #fff; border-radius: 12px; padding: 8px 12px 10px; text-align: center;
              box-shadow: 0 2px 8px rgba(0, 0, 0, .08); cursor: pointer; }
  .word { font-size: 34px; line-height: 2; }
  .meaning { font-size: 13px; color: #6b7280; }
  rt { font-size: 45%; color: #b4471f; user-select: none; }  /* copying gives the kanji only */

  /* quiz mode: readings are hidden until the card is tapped */
  .quiz li:not(.shown) rt { visibility: hidden; }
  .quiz li:not(.shown) { background: #fffbeb; }
</style>
</head>
<body>
<div class="bar">
  <button id="mode">Hide readings</button>
  <span id="score"></span>
</div>

<ul class="cards" id="cards" lang="ja">
  <li><div class="word"><ruby>猫<rp>(</rp><rt>ねこ</rt><rp>)</rp></ruby></div><div class="meaning">cat</div></li>
  <li><div class="word"><ruby>学<rp>(</rp><rt>がっ</rt><rp>)</rp>校<rp>(</rp><rt>こう</rt><rp>)</rp></ruby></div><div class="meaning">school</div></li>
  <li><div class="word"><ruby>電<rp>(</rp><rt>でん</rt><rp>)</rp>車<rp>(</rp><rt>しゃ</rt><rp>)</rp></ruby></div><div class="meaning">train</div></li>
  <li><div class="word"><ruby>今日<rp>(</rp><rt>きょう</rt><rp>)</rp></ruby></div><div class="meaning">today</div></li>
  <li><div class="word"><ruby>友<rp>(</rp><rt>とも</rt><rp>)</rp>達<rp>(</rp><rt>だち</rt><rp>)</rp></ruby></div><div class="meaning">friend</div></li>
  <li><div class="word"><ruby>水<rp>(</rp><rt>みず</rt><rp>)</rp></ruby></div><div class="meaning">water</div></li>
</ul>

<script>
  const cards = document.getElementById('cards');
  const items = cards.querySelectorAll('li');
  const mode = document.getElementById('mode');
  const score = document.getElementById('score');

  function update() {
    const quiz = cards.classList.contains('quiz');
    const shown = cards.querySelectorAll('li.shown').length;
    score.textContent = quiz ? shown + ' / ' + items.length + ' revealed' : '';
  }

  // switch between study mode and quiz mode
  mode.addEventListener('click', () => {
    const quiz = cards.classList.toggle('quiz');
    items.forEach((li) => li.classList.remove('shown'));
    mode.textContent = quiz ? 'Show all readings' : 'Hide readings';
    update();
  });

  // in quiz mode, tapping a card reveals (or hides again) its reading
  items.forEach((li) => {
    li.addEventListener('click', () => {
      if (!cards.classList.contains('quiz')) return;
      li.classList.toggle('shown');
      update();
    });
  });
</script>
</body>
</html>
Hide the readings, then tap a card to reveal its reading. The counter shows how many you have opened.
  • Quiz mode: one class on the list hides every rt with visibility: hidden.
  • Reveal: tapping a card adds a shown class that brings its readings back.
  • Clean copy: user-select: none on rt keeps the reading out of copied text.

Ruby for abbreviations and other notes

Ruby is not limited to East Asian text. The HTML spec describes it for short annotations in general, so a gloss over a word or a translation over a phrase is valid markup.

An abbreviation is a weaker fit. The expansion becomes part of the line, so it is copied and read along with the short form. For that job, the abbr tag with the full words written out on first use is the usual choice.

When it does not work

What you see Cause Fix
Readings press into the line above line-height is too tight for the extra text Set line-height to about 2 on text that carries ruby
Brackets show around readings A CSS rule gives rp a display value Remove rp from that rule; browsers hide it by default
ruby-position does nothing Set on rt, or an unsupported value such as inter-character Set over or under on ruby or a parent
Empty boxes instead of characters No installed font covers the script Use a system font stack and set lang
Characters look slightly wrong Missing or wrong lang Set lang="ja", lang="zh-CN" or lang="zh-TW"
Pasted text has readings mixed in rt is copied with the base text user-select: none on rt

For more on line spacing, see CSS line-height.

A page with ruby is worth seeing in a browser. A screenshot cannot hide readings or run a quiz, and an .html attachment may open as plain code on a phone.

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 people you send it to can tap the flashcards themselves. If you change the code later, the same link shows the new version.

Questions people ask

What is the ruby tag in HTML?

It marks base text that has a short annotation, usually a pronunciation guide. The annotation goes in an rt element inside the ruby element, and browsers draw it in small type above the base text. It is used for Japanese furigana and Chinese pinyin or zhuyin.

What do rt and rp mean?

rt is ruby text: the reading itself. rp is ruby parenthesis: a bracket placed around each rt. Browsers that lay out ruby hide rp. Where ruby layout is missing, the rp brackets make the result read as kanji (kana) instead of kanji and kana run together.

Should I add one rt per character or one per word?

Per character when each kanji has its own reading, such as the two kanji of Tokyo, so the reader sees which part says what. One rt for the whole word when the reading belongs to the word and cannot be split, such as the word for today, read kyou.

How do I put ruby text below the base text?

Set ruby-position: under on the ruby element or on a parent. The default is over. Put the property on ruby, not on rt.

Can I use ruby to show what an abbreviation stands for?

It works visually, but the expansion becomes part of the text on the line, so it is copied and read along with the abbreviation. The abbr element with the full form written out on first use is the usual tool for abbreviations.

Keep reading