To draw a triangle in pure CSS you have two good options. The border trick gives you a fixed-size triangle out of a zero-size element, and it is the right tool for a tooltip arrow. clip-path: polygon() cuts a triangle (or any shape) out of a real box, and it is the right tool when you need the shape to scale, rotate, or carry a background. Here is both, and when to reach for each.
/* The border trick: a triangle pointing up */
.triangle-up {
width: 0;
height: 0;
border-left: 12px solid transparent;
border-right: 12px solid transparent;
border-bottom: 16px solid #2563eb;
}That single rule renders a solid blue triangle pointing up. No SVG, no image, no extra markup. Both techniques below are production-safe today: the border trick has worked in every browser for over a decade, and clip-path: polygon() is Baseline (supported in Chrome, Edge, Firefox, and Safari, including their mobile versions).
Why the border trick draws a triangle
The trick is easier to keep straight once you see why it works. Borders on a box do not meet at right angles, they meet on a diagonal. Picture the four borders of a normal box: where the top border meets the left border, the seam runs at 45 degrees. On a box with real content, you never notice, because the borders are thin. But shrink the box to width: 0; height: 0 and the four borders collapse into four triangles that share a single point in the middle, each one fanning out to a corner.
Make three of those four borders transparent and you are left with one visible triangle. The colored border is the base of the triangle, and it points away from that side: a colored border-bottom produces a triangle pointing up, because the bottom border's triangle fans upward toward the center.
So the direction rule is the opposite of what you would guess. Color the side opposite the direction you want it to point:
/* Pointing up: color the bottom */
.up {
width: 0; height: 0;
border-left: 12px solid transparent;
border-right: 12px solid transparent;
border-bottom: 16px solid #2563eb;
}
/* Pointing down: color the top */
.down {
width: 0; height: 0;
border-left: 12px solid transparent;
border-right: 12px solid transparent;
border-top: 16px solid #2563eb;
}
/* Pointing right: color the left */
.right {
width: 0; height: 0;
border-top: 12px solid transparent;
border-bottom: 12px solid transparent;
border-left: 16px solid #2563eb;
}
/* Pointing left: color the right */
.left {
width: 0; height: 0;
border-top: 12px solid transparent;
border-bottom: 12px solid transparent;
border-right: 16px solid #2563eb;
}The two transparent borders control the base width (half each), and the colored border controls the height. Equal values give you a roughly equilateral look; a taller colored border gives a sharper point.
One small gotcha worth knowing: use transparent, not border-left: 0. Setting the side borders to zero collapses the shape instead of leaving the triangular gap, and on some older rendering paths a dark hairline can show along the diagonal seam. transparent keeps the geometry intact and the edges clean.
The real use case: a tooltip arrow
Standalone triangles are a novelty. Where the border trick earns its keep is the little arrow that points from a tooltip or popover back at the thing it describes. The clean pattern is a ::before or ::after pseudo-element, so the arrow needs no extra HTML:
.tooltip {
position: relative;
background: #1f2937;
color: #fff;
padding: 8px 12px;
border-radius: 6px;
}
/* The arrow, hanging off the bottom edge, pointing down */
.tooltip::after {
content: "";
position: absolute;
top: 100%; /* sit just below the box */
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-top: 8px solid #1f2937; /* same color as the tooltip */
}Match the arrow's colored border to the tooltip's background and it reads as one shape. If the tooltip has a border of its own, the classic move is two stacked triangles: a slightly larger arrow in the border color sitting one pixel behind a slightly smaller arrow in the fill color, so the border appears to wrap the arrow too. It is fiddly but it is how most hand-rolled tooltips do it.
The modern alternative: clip-path: polygon()
clip-path clips a real element down to a shape you define with coordinate pairs. For a triangle you give it three points, each an x y pair as a percentage of the box:
.triangle-clip {
width: 40px;
height: 40px;
background: #2563eb;
/* top-center, bottom-right, bottom-left -> points up */
clip-path: polygon(50% 0, 100% 100%, 0 100%);
}The points are listed clockwise. 50% 0 is the top-center apex; 100% 100% and 0 100% are the two bottom corners. Reorder them and you point the triangle anywhere. Because the clip is applied to a normal box, this approach has real advantages over the border trick:
- It scales. The shape is defined in percentages, so resize the box and the triangle resizes with it. The border trick is locked to pixel border widths.
- It can be any shape. Swap in more points for arrows, chevrons, stars, or speech bubbles.
polygon(0 0, 100% 50%, 0 100%, 25% 50%)is a right-pointing arrowhead. - It carries a background. The clipped box keeps its
background, including gradients and images, so you can have a triangle with a gradient fill, which the border trick cannot do. - It animates and rotates cleanly. You can transition between two
polygon()shapes (with the same point count) or rotate the element withtransform.
The one thing to watch is edge quality: on some setups the diagonal cut from clip-path can look very slightly softer than a border edge at small sizes. For a tiny tooltip arrow the border trick is still crisper and simpler; for anything that needs to scale, fill, or morph, reach for clip-path.
Which one to use
| Need | Use |
|---|---|
| A fixed-size tooltip or dropdown arrow | Border trick (on ::before/::after) |
| A solid triangle at one known size | Either; border trick is the shorter rule |
| A triangle that scales with its container | clip-path: polygon() |
| A triangle with a gradient or image fill | clip-path: polygon() |
| A shape that animates or rotates | clip-path: polygon() |
| Any non-triangle shape (chevron, star, badge) | clip-path: polygon() |
Both are pure CSS, both ship everywhere today, and neither needs an image. The border trick is the older muscle memory and still the cleanest answer for arrows; clip-path is the one that scales with modern layout.
If you reach for clip-path a lot, it pairs naturally with CSS filters like drop-shadow(), which (unlike box-shadow) follow the clipped silhouette instead of the original box.
FAQ
See also
- CSS Filters: blur, grayscale, drop-shadow, and more:
drop-shadow()follows a clip-path silhouette, whichbox-shadowcannot. - CSS content and attr(): generated content from attributes: the
::before/::afterpseudo-elements the tooltip arrow hangs off can also pull text straight from attributes. - The CSS :has() parent selector: style a tooltip or its arrow based on what the element contains, no extra classes.
Sources
Authoritative references this article was fact-checked against.
- border shorthand property (official MDN docs)developer.mozilla.org
- clip-path CSS property (official MDN docs)developer.mozilla.org
- polygon() CSS function (official MDN docs)developer.mozilla.org
- CSS clip-path browser support (Can I use)caniuse.com





