Skip to content
ColorKit

Free CSS arrow generator

Arrows that attach to the bubble

This generator is free and asks for no account: choose which of the four edges the pointer leaves from, slide it anywhere along that edge, and out come the pseudo-element rules that fasten it to a bubble you already have — with no extra div in your markup. When the bubble has a border, the arrow is two stacked triangles and the back one is scaled by a computed factor rather than padded by a couple of pixels, so a 1px border reads as a full 1px of rim along the slant instead of the 0.77px a base-plus-2px expansion really produces. Every arrow is emitted a second way as well, as one clipped box, for the bubbles the border trick cannot serve.

  • 100% free
  • No signup
  • 4 edges
  • Outline that fits
  • Two techniques
Edge the arrow grows out of

Press Ctrl+V anywhere on the page to drop in a hex code for the bubble — no need to click the field first.

Technique in the preview
Saved to your workspace. Undo is available for 30 seconds.
Outline scale
× 1.0468
Outline triangle
20.94 × 12.56 px
Corner clearance
18.47 px
Rim if scaled naively
0.77 px

Widening the outline to base + 2×1 and length + 1 instead would lay 0.77px of color where the arrow meets the bubble, against the 1px the bubble's own border has there — the expansion is horizontal and the edge it has to match is not.

Border triangles

.tooltip {
  position: relative;
  display: inline-block;
  max-width: 260px;
  padding: 10px 14px;
  border: 1px solid #a5b4fc;
  border-radius: 8px;
  background: #ffffff;
  color: #000000;
}

.tooltip::before,
.tooltip::after {
  content: "";
  position: absolute;
  width: 0;
  height: 0;
  border-style: solid;
  pointer-events: none;
}

/* outline: scaled by 1.0468 so the rim stays 1px across the diagonal */
.tooltip::before {
  top: calc(100% + 1px);
  left: 50%;
  margin-left: -10.47px;
  border-width: 12.56px 10.47px 0 10.47px;
  border-color: #a5b4fc transparent transparent transparent;
}

/* fill: sits on the padding edge, which is what hides the bubble's own border behind it */
.tooltip::after {
  top: 100%;
  left: 50%;
  margin-left: -10px;
  border-width: 12px 10px 0 10px;
  border-color: #ffffff transparent transparent transparent;
}

Clipped box

.tooltip {
  position: relative;
  display: inline-block;
  max-width: 260px;
  padding: 10px 14px;
  border: 1px solid #a5b4fc;
  border-radius: 8px;
  background: #ffffff;
  color: #000000;
}

.tooltip::after {
  content: "";
  position: absolute;
  top: 100%;
  left: 50%;
  margin-left: -10px;
  width: 20px;
  height: 12px;
  background: #ffffff;
  clip-path: polygon(0 0, 100% 0, 50% 100%);
  pointer-events: none;
}

Both blocks describe the same 20 × 12 px triangle in the same place; only the outline and the ability to carry a background image differ. Keep position: relative on the bubble — the arrow is measured from its padding box, and an unpositioned parent hands the job to whatever is above it.

How to attach an arrow to a tooltip

Edge, size, colors — and one property on the bubble that the whole thing depends on.

  1. Say which edge it leaves from, and where along it

    The edge buttons name the side of the bubble the pointer grows out of, so a tooltip floating above the thing it describes wants the bottom edge. The offset slider is a percentage of that edge measured on the padding box, which matters when the bubble is generously padded: 50% is the middle of the padding box, not the middle of the text. Keep an eye on the corner-clearance figure while you drag — it is the bubble's radius plus half the widest triangle, and inside that distance the arrow starts eating into the rounded corner.

  2. Match the bubble's fill, border and radius

    Type the bubble's background and the arrow is handed the same value, a readable label color is chosen from it by relative luminance, and if the border width is above zero the outline triangle is worked out too. The border slider runs to 8px; at zero the second pseudo-element is dropped from the output altogether rather than emitted with a zero-width border, because a rule that does nothing is a rule somebody has to read later.

  3. Paste both blocks and leave position: relative alone

    The first block is the bubble and the rest are its pseudo-elements, so if you already style the bubble, take only the ::before and ::after rules and make sure your own rule keeps position: relative — absolutely positioned children are measured from the nearest positioned ancestor, and without one the arrow flies off to the viewport. That same rule is why the fill triangle needs no nudging to cover the bubble's border: top: 100% resolves against the padding box, so the triangle already begins on the inside edge of the border and hides the segment it crosses.

Technical specifications

Edges4, with the arrow placed anywhere from 0% to 100% along the one you pick
Arrow sizeBase 6-80 px and length 4-48 px, set independently, so the point can be blunt or needle-thin
Outline scaleouter = inner × (1 + r ÷ d − r ÷ length), where d = (base ÷ 2 × length) ÷ √(length² + (base ÷ 2)²); at base 20, length 12 and a 1 px border that comes to 1.0468
Elements added to your HTMLNone — ::after alone on a borderless bubble, ::before and ::after when it is outlined
Second outputThe same triangle as one clipped box: a single ::after of 10 declarations with a three-vertex clip-path polygon
Label colorChosen between black and white from the bubble color by WCAG relative luminance; white takes over below 0.179
Browser supportThe border pair works wherever transparent borders do; clip-path polygon on a pseudo-element needs Chrome 55, Firefox 54 or Safari 13.1 unprefixed
RunsOn values you type, in your own browser — no bubble markup is sent anywhere

Frequently asked questions

Why does the bubble's border run straight across the base of the arrow?

Because the fill triangle is starting outside the border instead of on the padding edge. An absolutely positioned pseudo-element is laid out against its parent's padding box, so a plain top: 100% lands exactly on the inner edge of the bottom border, where the triangle covers the border line for its own width and the join reads as one shape. Snippets that write top: calc(100% + 1px), or that wrap the bubble in a box-sizing: content-box container, push the triangle past the border and leave the line showing.

How close to a rounded corner can the arrow sit?

No closer than the radius plus half the widest triangle, which the corner-clearance readout gives you in pixels for the current settings. Nearer than that and the base of the arrow overlaps the curve, so part of it attaches to nothing and a notch of page background appears between bubble and pointer. At the 8px radius and 20px base this page starts with, the arrow's centre needs 18.47px of clearance, which on a 260px bubble is roughly the 7% and 93% marks on the offset slider.

Can I write one arrow rule and rotate it for the other three edges?

You can, with transform: rotate() on the pseudo-element, and it is usually a false economy. Rotation is applied after layout, so the element still occupies its unrotated box and the offsets that positioned it no longer describe where it ends up — you finish by correcting with a translate as well. Four small rules that each state plainly where they are beat one rule plus a transform you have to re-derive at every call site.

My bubble has a gradient background and the arrow is a flat color.

border-color accepts a color and cannot accept an image, so the border technique physically cannot continue a gradient. Switch to the clipped box and give the pseudo-element the same gradient with background-size set to the bubble's size and a negative background-position matching where the arrow sits, so the ramp carries on instead of restarting inside the arrow. That is exact for a linear gradient at any angle and approximate for a radial one, where the centre of the ramp also has to move.

Do the pseudo-elements interfere with hover and clicks?

They extend the bubble's hit area, because a generated box belongs to the element that generated it — hovering the arrow counts as hovering the bubble, and so does hovering the transparent corners of the triangle's rectangular box. The output sets pointer-events: none on both, which is right for a tooltip that must not steal the cursor from what is underneath, and wrong for a popover that should stay open while the pointer travels across the arrow; delete the line in that case.

Why does the arrow look darker than the bubble when the bubble is translucent?

Because the two triangles overlap and alpha compounds: the fill sits directly on top of the outline, so every pixel of the arrow is two translucent layers where the bubble is one. Either use the clipped box, which paints a single layer, or keep the bubble's own color opaque and move the transparency to a wrapper that covers both.

Can the arrow point diagonally, out of a corner?

Not from these four edge anchors — a corner pointer is a different shape, normally a right-angled wedge whose two straight sides continue the bubble's own edges rather than an isosceles triangle standing on one of them. Build that as a corner wedge on the triangle generator and place it with the same absolute offsets; the outline arithmetic here does not carry over, because a wedge that meets the bubble along two sides needs a rim on one diagonal only.

About tooltip arrows in CSS

The arrow on a tooltip is not a shape of its own. It is the border triangle — an element with no content box, whose borders mitre into four wedges — pinned to a bubble with absolute positioning. Everything that makes it an arrow rather than a triangle is positional: which edge it hangs off, how far along that edge it sits, and the fact that it lives in ::before and ::after so the bubble stays a single element in the DOM. Getting those right is most of the work, and the part people get wrong is the containing block, because an absolutely positioned child is measured from its parent’s padding box: 100% of the way down is the inside of the bottom border, not the outside of it.

The outlined case is where generators diverge, and the difference shows at any border thicker than a hairline. Surrounding a triangle with a rim of even thickness needs a second triangle whose slanted edges are parallel to the first pair and offset from them perpendicularly, and that is a uniform scale, not an addition. Adding the border width to the half-base and to the length — the recipe in most tooltip snippets — widens the shape faster than it lengthens it, so the two slants tilt towards each other and the rim thins as it approaches the bubble: 0.51px of a nominal 1px border at a 40px base and a 12px length. This page solves for the scale factor from the distance between the two parallel slant lines and prints it in the output so you can check the arithmetic. The fill triangle is then pulled back by exactly the border width, which is what lets it cover the bubble’s own border where the arrow crosses it, and the factor absorbs that pull-back so the rim still measures true.

Three situations break the border pair, and all three end at the clipped box. A bubble filled with a CSS gradient cannot hand its fill to border-color, which takes colors only. A bubble carrying a box shadow casts that shadow from its own rectangle, so the arrow appears to float in front of an unbroken edge — the repair is filter: drop-shadow() on a wrapper, which traces the union of bubble and arrow instead. And a heavily rounded bubble, or a button-shaped popover with hover and focus states to match, usually wants the arrow to open along with the bubble; a clipped box transitions between polygons that share a vertex count and keeps a real width and height while it does, whereas animating four border widths moves the apex as a side effect, because the apex sits wherever the left border happens to end.

What happens to the bubble you describe

Every number on this page is worked out by JavaScript running in the tab you are reading it in. Nothing you type, paste or open is uploaded, logged or kept, which is also why the tools carry on working after you disconnect from the network.

The colors you type and the numbers you drag never become a request. The preview is styled by a style element this page writes into itself from the same string that fills the code box, so what you are looking at is the output rather than a hand-built imitation of it.