Why an element with no size still draws something
The trick is older than most of CSS's shape tools and still the lightest way to draw an arrow.
An element with no width and no height still draws its four borders. Because those borders meet at diagonals, each one is not a rectangle but a wedge, with its point at the element's centre. Make three of them transparent and the fourth is left standing alone: a triangle.
No image, no SVG, no extra markup. The [msm_tool slug="css-triangle"] writes the border widths and colours for each direction, but the mechanism is worth understanding because it explains every quirk that follows.
The colour goes on the opposite side to the point
This is the part that catches everyone, and it follows directly from the wedges.
A triangle pointing up is drawn by colouring the bottom border. The bottom border's wedge has its point at the top and its base along the bottom edge, so that is the shape left behind when the others go transparent.
Once that clicks, the rest of the property follows without needing to be memorised.
The base is twice the value you set
The left and right borders each contribute half of an upward triangle's base. Set both to 20 pixels and the finished triangle is 40 pixels wide.
That doubling is the second most common source of confusion, and it is why the generator reports the finished width alongside the value rather than leaving you to do the arithmetic.
Setting the base and the height independently is what allows scalene and tall narrow triangles. A single size value only ever gives a right-angled or isosceles one.
It cannot take a border or a gradient
The shape is made of borders, so there is nothing left over to draw an outline with. There is no fill to apply a gradient to either — the triangle is a solid colour by construction.
Two workarounds:
- For an outline, stack two triangles: a slightly larger one in the outline colour behind a smaller one in the fill colour, offset by a pixel or two. Fiddly but reliable.
- For a shadow, use
filter: drop-shadow(), which follows the visible shape rather than the element's rectangle. box-shadow would draw a shadow around the invisible box.
If you need either of those regularly, a clip-path or an inline SVG is the less painful route.
Where it is still the right choice
Tooltip and popover arrows, above all. Place the triangle on a pseudo-element positioned against the container's edge, with its colour matching the container's background, and it reads as part of the shape.
Also: dropdown carets, breadcrumb separators, the notch on a speech bubble, and the small indicator on an accordion. Anything where you want a simple arrow with no extra element and no request.
Triangle or clip-path
The border trick for simple arrows — it is smaller, supported everywhere without exception, and needs no vendor prefix. A clip-path when the shape is more complex, needs its own outline, or has to animate.
The border version cannot animate its shape smoothly, because transitioning border widths produces a stepped result rather than a morph.
A note on the markup
Triangles built this way are decoration, and they belong on a pseudo-element rather than a real one. An empty <div> whose only purpose is to be a shape is an element a screen reader has to step over for no reason.
::before or ::after on the element that owns the arrow keeps the markup clean and keeps the decoration out of the accessibility tree entirely.