Badge

Badges show notifications, counts, or status information on navigation items and icons.

Usage

Badges show notifications, counts, or status information on navigation items and icons. Material 3 places them on the destinations of a navigation rail or bar, and that is where NavigationRailItem puts one: its badge input takes the badge’s own contract – {} for the dot, { label: 3 } for a count.

Material recommends clearing a badge once its destination has been viewed. That is the caller’s decision: the example drops the badge when its item is selected, and the New message action brings it back. Both ends are animated – the badge scales and fades in from nothing and back out – by one anime.js controller in @udixio/core/dom that both frameworks connect.

Material 3 describes two variants, and what separates them is only whether the badge carries text, so the variant follows from the content rather than from a prop: with no label the badge is the small dot that signals an unread notification, and any label makes it the large pill that carries a count. A small badge showing a count is not a state the design has, so it is not a state the API can express.

On an icon

Outside a rail, the badge marks an icon directly. The two frameworks deliver it in the shape each one is built for:

  • React exports a Badge component that wraps the icon and provides the positioned container itself.
  • Angular exports a Badge directive, [udxBadge], that you put on the icon. It places the badge inside the element it sits on and makes that element the containing block, so nothing is wrapped. Its inputs are prefixed udxBadge* because the host element is not the directive’s own.

The contract, the measurements, the DOM the badge renders and the accessibility are identical; only the way you reach them differs. That holds for classes too: the container element of the style contract is the wrapper in React and the marked element in Angular, so the string form – className="mt-4", udxBadgeClass="mt-4" – lands on the wrapper in one and on the icon in the other, and a container override does the same.

Unread messages3 unread messages999 or more notifications

Counts

max caps a numeric label the way Material spells it: label={1000} with max={999} renders 999+. Material limits badge content to four characters including that +, which 999+ reaches exactly.

Nothing truncates a label that exceeds it, and nothing clamps the width either. Material gives 34dp as the size a large badge reaches at four characters, in their own font; ours renders 999+ a pixel wider, so clamping to it cut the label to 99…. The badge grows with its content instead, and the four-character limit stays guidance. Silently dropping a caller’s text would hide data; max is the tool for the count case, and a longer string is a deliberate choice the component does not override.

A count of 0 renders. Treating it as absent would turn an explicit “nothing unread” into the dot that means “something unread”.

Placement

The badge anchors inside the bounding box of what it marks, at the upper trailing corner, using the distances Material specifies: 6×6dp for the small dot, 14×12dp for the large pill. The leading edge is what is pinned, so a widening count grows outward without the badge moving.

Mark the icon, not a control around it. Material anchors badges inside the icon bounding box, and a control with a large touch target – an icon button carries 56dp around a 24dp icon – anchors the badge to that target instead, pushing it away from the icon by the padding.

In Angular the badge lives inside the element it marks, so that element – or, for a display: contents host such as udx-icon, the first box inside it – must accept children; an <img> or an <input> cannot carry one. The element is made position: relative only if it was static, and only that is undone when the directive goes away.

Offsets are expressed with logical properties, so the badge moves to the upper leading corner in right-to-left languages, as the guidelines require.

Colour

The container uses the error role and the label on-error. Material asks that the default mapping be kept – the badge is meant to stand out against labels, icons and navigation items – and that any substitute keep a contrast ratio of at least 3:1.

Accessibility

description is what assistive technology announces, and it should carry the meaning rather than the figure: 3 unread messages, not 3. The small dot has no text at all, so description is its only way to exist for a screen reader.

Without a description the badge is hidden from the accessibility tree. A bare digit, or an empty dot, announces noise rather than information.

Hiding the badge once its destination is selected is the caller’s decision – Material recommends it for the unread-notification case. Set visible to false (udxBadgeVisible in Angular) rather than unmounting it: the badge animates out, stays in the DOM with its last content, and leaves the accessibility tree until it is shown again. NavigationRailItem does exactly that when its badge goes back to undefined.

Motion

Showing and hiding scale and fade the badge, 200ms outCubic by default; transition (udxBadgeTransition) changes the duration and easing. A prefers-reduced-motion user gets the end state at once, and so does the first paint, so a page never opens with badges popping in.