The Classic Border Trick
The oldest trick: a zero-size element with thick borders. Each border is a trapezoid; making three transparent leaves just one triangle.
/* Triangle pointing up */
.triangle-up {
width: 0;
height: 0;
border-left: 20px solid transparent;
border-right: 20px solid transparent;
border-bottom: 30px solid #7c5cfc;
}
/* Triangle pointing down */
.triangle-down {
width: 0;
height: 0;
border-left: 20px solid transparent;
border-right: 20px solid transparent;
border-top: 30px solid #7c5cfc;
}
/* Triangle pointing right */
.triangle-right {
width: 0;
height: 0;
border-top: 20px solid transparent;
border-bottom: 20px solid transparent;
border-left: 30px solid #7c5cfc;
}
/* Triangle pointing left */
.triangle-left {
width: 0;
height: 0;
border-top: 20px solid transparent;
border-bottom: 20px solid transparent;
border-right: 30px solid #7c5cfc;
}
Why it works: when an element has width and height of 0, its borders meet at diagonal seams. The non-transparent border you keep becomes a triangle pointing away from itself. Adjust border widths to change triangle proportions — equal left/right = isosceles; unequal = scalene.
Modern Alternative: clip-path
The clip-path approach treats the element as a real box and clips it to a polygon. Code is more intuitive and supports rotation, scaling, and transitions.
.triangle-up {
width: 40px;
height: 30px;
background: #7c5cfc;
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
}
.triangle-down {
width: 40px;
height: 30px;
background: #7c5cfc;
clip-path: polygon(0% 0%, 100% 0%, 50% 100%);
}
.triangle-right {
width: 30px;
height: 40px;
background: #7c5cfc;
clip-path: polygon(0% 0%, 100% 50%, 0% 100%);
}
.triangle-left {
width: 30px;
height: 40px;
background: #7c5cfc;
clip-path: polygon(100% 0%, 0% 50%, 100% 100%);
}
Advantages of clip-path: (1) the element has real dimensions, so you can apply padding, add content, use flexbox children; (2) easy to animate with transform; (3) readable code — the polygon points are obvious.
Speech Bubble with Triangle Tail
The most common use: adding a tail to a chat bubble or tooltip.
.bubble {
position: relative;
background: #7c5cfc;
color: #fff;
padding: 12px 16px;
border-radius: 12px;
max-width: 300px;
}
.bubble::after {
content: '';
position: absolute;
bottom: -10px;
left: 20px;
width: 0;
height: 0;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 10px solid #7c5cfc;
}
Key pattern: use ::before or ::after pseudo-elements so you don't need extra HTML. Match the triangle color to the bubble background color, and position with position: absolute.
Tooltip with Triangle Pointer
.tooltip {
position: relative;
display: inline-block;
}
.tooltip:hover::after,
.tooltip:hover::before {
opacity: 1;
}
.tooltip::after {
content: attr(data-tooltip);
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
padding: 6px 10px;
background: #1a1a1a;
color: #fff;
border-radius: 6px;
font-size: 12px;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
}
.tooltip::before {
content: '';
position: absolute;
bottom: calc(100% + 2px);
left: 50%;
transform: translateX(-50%);
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 6px solid #1a1a1a;
opacity: 0;
transition: opacity 0.2s;
}
Usage: <button class="tooltip" data-tooltip="Hello world">Hover me</button>
Triangle with Outline / Border
Creating a triangle with a visible outline is where border triangles break down (borders are already used up). Use clip-path or stack two triangles:
.triangle-outlined {
width: 40px;
height: 35px;
background: #1a1a1a; /* outline color */
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
position: relative;
}
.triangle-outlined::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
right: 2px;
bottom: 0;
background: #7c5cfc; /* fill color */
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
}
Comparison: Which Approach When
- Border trick — best for simple tooltip/bubble tails via pseudo-elements. Maximum browser support. Limitation: can't contain content, can't transition shape smoothly.
- clip-path — best for modern UIs. Readable code, real dimensions, animatable. Supported in all evergreen browsers since 2018. Use for carousel arrows, ribbon badges, cut-corner cards.
- SVG triangle — best when you need precise control, stroke, gradients, or interactive behavior. Slightly heavier.
- Unicode characters (▲ ▼ ◀ ▶) — acceptable for text-like arrows but limited styling.
Frequently Asked Questions
- Why use CSS triangles instead of an SVG or image?
- Zero HTTP requests, infinitely scalable (with clip-path), inherit
colorviaborder-color: currentColor, and trivially themeable via CSS variables. An SVG triangle is fine too, but CSS triangles integrate more naturally with pseudo-elements for speech bubbles. - Can I animate a CSS triangle's size or rotation?
- Rotation: yes, via
transform: rotate()— works on both border and clip-path triangles. Size animation: smooth only with clip-path (animatingtransform: scale()) — border triangles can't transition their border-width cleanly. - How do I make the triangle responsive to its parent?
- Use clip-path with percentage units — the triangle auto-scales to any width/height. Border triangles use fixed pixel border widths, so they don't scale without JavaScript or CSS variables.
- What browsers support clip-path for triangles?
- All evergreen browsers since 2018 (Chrome 55+, Firefox 54+, Safari 9.1+). Polygon syntax is universally supported. Only IE11 needs a fallback — and IE is no longer a concern for most sites in 2026.
- How do I center a triangle under a button?
- For pseudo-element tails:
position: absolute; left: 50%; transform: translateX(-50%);on the triangle. For standalone triangles in a flex container:margin: 0 autoor parentjustify-content: center.