When to hang a banner off a card rather than putting a Tag inside it, what the label and the color are allowed to say, and what the surface underneath has to allow for.
When to use
A ribbon is a banner that rides the corner of a card and says one thing about the whole card: new, most popular, beta. Import it as Ribbon from @/components/ui/ribbon.
Use it when the label is about the card as a whole and should be read before the card's own title: the highlighted plan in a pricing table, the one template in a gallery that is new, the feature card that is still in beta.
Use something else when
The label describes something inside the card rather than the card itself. That is a Tag, placed next to the thing it qualifies, where the reading order puts it beside its subject.
It is a count or a dot on a corner. That is Badge, which is built for a number and stays legible at one character.
It is a status the reader has to act on. That is an Alert inside the card, which can carry the explanation and the action. A ribbon can carry neither.
There is nothing to wrap. Ribbon positions itself against its child, so it always needs one: <Ribbon text="New"><Card /></Ribbon>, never a bare ribbon.
The label
One or two words. The label does not wrap and already hangs 8px past the card's edge, so a long label grows further outward.
A noun or a short adjective, not a sentence and not a date: “New”, “Beta”, “Most popular”.
Parallel across a set. Ribbons in one grid are compared with each other, so keep them the same part of speech and roughly the same length.
text takes any node, so an icon can ride along, but keep the word. The glyph is not the label.
The label is already one step smaller than body text, so it is the smallest text on the card. Do not shrink it again.
Color and emphasis
With no color the ribbon paints colorPrimary. That is the right default when the ribbon means “this one”, and it keeps a grid down to a single accent.
color takes antd's presets (blue, green, gold and the rest), a palette from your theme (the default theme adds amber, emerald, teal, sky, indigo, violet, fuchsia, rose and grays like slate and zinc), or any CSS color.
A preset name wins even when it is also a CSS keyword, like red. A hex, an rgb() or a var(--ant-*) value is painted as given.
Decide what a hue means on the surface and keep it, the way the status colors already mean something. A color picked per card for variety teaches the reader nothing and costs contrast.
emphasis is the loudness axis, not the color. default is a banner: the color as the fill with a light label, meant to be seen first. subtle moves the color to a tint and to the ink, so the ribbon reads as a marker under the card's own title.
subtle needs a color ramp. Presets and theme palettes have one. A hex or rgb() color has none, so the ribbon keeps its solid fill at subtle instead of showing light text on a light fill.
Check subtle text on yellow and gold. Its label misses AA contrast on those hues, so keep it a marker beside the card's own text, not the only place the meaning lives.
Placement
end is the default and is the corner readers look at last, which suits a label that qualifies a card they have already started reading.
start and end follow the text direction, so they swap in a right-to-left layout, and the fold swaps with them.
Pick one per surface and keep it. A grid whose ribbons alternate corners reads as noise, because placement then carries no meaning of its own.
Reach for start when the card's end corner is already spoken for by a menu button, a checkbox or an avatar.
The surface it wraps
Ribbon wraps the child in a position: relative div and places the banner 8px down from its top and 8px outside its edge. The fold hangs from that overhang, and the layout around it has to leave room for it.
An ancestor with overflow: hidden clips the overhang and the fold with it, and takes a bite out of the banner. Check any scroll container, any masked media header and any card whose own radius is enforced by clipping.
Wrap something with an edge. A ribbon on a background-less block has nothing to hang off and floats.
The wrapper is a plain block, so sizing belongs on the card inside it. In a grid, put h-full on the card rather than on the ribbon.
Leave the row enough gap that the 8px overhang does not touch the neighboring cell.
The banner paints over the card, so keep the first band of that corner clear: a card title that runs the full width will end up underneath it.
Sharing the corner
One ribbon per card. The corner is a single slot, and a second banner has nowhere to go that does not collide with the first.
Not every card in a grid. A ribbon on all three plans says nothing, so give one only to the plan you want to highlight.
If the card already carries a menu, a close button, a checkbox or a Badge count in that corner, move one of them or flip placement. Do not layer them.
A count belongs inside the card, beside what it counts. A ribbon and a Badge on the same corner read as one broken control.
Should a ribbon be clickable
No. The ribbon has no role, no tab stop and no onClick prop.
Reaching the element another way and attaching a handler builds a control the keyboard cannot reach and assistive tech does not announce. Put the action in the card.
A focusable element inside text does render and does take focus, but it comes last in the card's tab order and sits in a thin band over the corner. It is a small target in an unexpected place.
If the whole card is a link, let the card be the link. The ribbon then rides along as decoration, which is what it is.