Badge
الشارةrole="status" / badgeContent
also callednotification badge، count badge، dot badge، status badge، unread count، notification dot، بادج، عدّاد الإشعارات، النقطة الحمراء، شارة العدّ، شارة الحالة، عدّاد غير المقروء
A badge is a small mark attached to the corner of another element, and it reports one thing about that element: how many items are waiting inside it, or that something in it has not been seen. It is always dependent and never read on its own, so a badge over a mail icon describes the mail, not itself. A badge is not a chip: a chip is a control that sits in the tab order and changes page state when you press it, while a badge takes neither focus nor a press, and what gets pressed is the element underneath. It is not a tag either: a tag sits in the flow of text and classifies an article or a product whether or not anything changed, whereas a badge is anchored to the edge of one specific element and has no meaning without it. It is also not the presence dot on an avatar, which reports a standing state such as "online", while a badge dot reports something unread that goes away once it is read. What separates it from all three is ownership: a badge owns no action and no touch target of its own, and its wording belongs inside the anchor's accessible name. Badges come in two forms: a count badge carrying a number with an overflow cap, usually written 99+, and a dot badge with no number that says only that something is new.
If you called it…
Live specimen
Interact with the demo. Every part is real and numbered.
Anatomy: every part, named
Hover a row to locate it in the demo above.
Build prompt
How it behaves right-to-left
This section is ours alone.
Which corner it takes
mirrorsThe badge belongs at the logical inline end of the anchor's top edge: top right in English, top left in Arabic. Two negative logical insets do the whole job, inset-block-start: -6px with inset-inline-end: -6px, and the side moves on its own with no RTL rule. The popular recipe of inset-inline-end: 0 plus transform: translate(50%, -50%) mirrors only half of itself, because transform has no logical form at all: translate(50%) pushes right in both directions, outward in English and inward over the icon in Arabic, so sign the offset as translate(calc(50% * var(--dir)), -50%) with --dir set to -1 under [dir="rtl"].
The plus sign in the cap
never mirrorsIn a string like 99+ the plus sign carries bidi class ES, and rule W4 folds an ES into the number only when it falls between two European digits, which it does not here since nothing follows it. Left neutral, it takes the direction of the paragraph, so an Arabic page renders the string 99+ as +99. Isolate the digits and the sign together in one element carrying the dir="ltr" attribute: the attribute alone pulls unicode-bidi: isolate from the browser stylesheet, while CSS direction: ltr isolates nothing and has to be paired with unicode-bidi: isolate.
Which digits you ship
never mirrorsPick one numeral system for the whole product, Arabic-Indic U+0660 to U+0669 or Latin U+0030 to U+0039, and set it in one place with new Intl.NumberFormat("ar", { numberingSystem: "latn" }) instead of per component. The two are not the same thing in bidi either: Latin digits are class EN and Arabic-Indic digits are class AN, and rule W4 binds a plus or a minus between two EN digits only, so the same sign stays neutral among Arabic-Indic ones. Watch the font as well, since plenty of UI fonts carry no Arabic-Indic figures and the browser falls back inside the badge alone, and font-variant-numeric: tabular-nums does not convert between the systems, it only picks among the figure sets the font already has.
The dot and its offset
mirrorsA dot is a symmetric circle, so nothing in its shape mirrors, and a blanket [dir="rtl"] .icon { transform: scaleX(-1) } must never reach it. Its offset is a different matter: top: -2px with right: -2px keeps the dot in the physical top right, which is the start corner in Arabic, so it lands opposite every sibling badge built from logical properties in the same bar. Write the offset as inset-block-start and inset-inline-end, leave the box-shadow: 0 0 0 2px var(--surface) ring alone because it is symmetric, and give any one-sided clip-path a mirrored version, since clip-path has no logical form.
A badge after a text label
mirrorsA badge that follows a tab label is an element in the line flow rather than an absolutely positioned one, and its side comes from the line: after the word means to the left of it in Arabic. Set the space with margin-inline-start on the badge or gap on the container, never margin-left, which after the flip glues the badge to the word on one side and doubles the gap on the other. Keep the badge after the label in DOM order in both languages and let direction do the visual swap, because flex-direction: row-reverse flips a second time and leaves reading order disagreeing with the tree order a screen reader follows.
In code
Each row is one framework's word for the same thing. Take the row your project speaks.
See also
Updated · 2026-08-19