I'd like to create a speech bubble using pure CSS.
There's a few requirements I have:
Here's an example of what I would imagine two of such a speech bubble would look like, the first has a preferred with and height and centers the content, the second fits the height and width of the content with some padding. I think the crucial bit here is that in both cases, the little pointy triangle remains the same size and is centered.
I've tried looking for different speech bubble approaches, and attempted to adapt examples, such as #34 on the list here:
/* HTML: <div class="tooltip">This is a Tooltip with a border and a border radius. Border can have a solid or gradient coloration and the background is transparent</div> */
.tooltip {
color: #fff;
font-size: 18px;
max-width: 28ch;
text-align: center;
background-color: #ff000055;
border-radius: 25px;
}
.tooltip {
/* triangle dimension */
--a: 90deg; /* angle */
--h: 1em; /* height */
--p: 50%; /* triangle position (0%:left 100%:right) */
--b: 7px; /* border width */
--r: 1.2em; /* the radius */
padding: 1em;
color: #415462;
position: relative;
z-index: 0;
}
.tooltip:before,
.tooltip:after {
content: '';
position: absolute;
z-index: -1;
inset: 0;
background: conic-gradient(#4ecdc4 33%, #fa2a00 0 66%, #cf9d38 0); /* your coloration */
--_p: clamp(
var(--h) * tan(var(--a) / 2) + var(--b),
var(--p),
100% - var(--h) * tan(var(--a) / 2) - var(--b)
);
}
.tooltip:before {
padding: var(--b);
border-radius: var(--r) var(--r) min(var(--r), 100% - var(--p) - var(--h) * tan(var(--a) / 2))
min(var(--r), var(--p) - var(--h) * tan(var(--a) / 2)) / var(--r);
background-size: 100% calc(100% + var(--h));
clip-path: polygon(
0 100%,
0 0,
100% 0,
100% 100%,
calc(var(--_p) + var(--h) * tan(var(--a) / 2) - var(--b) * tan(45deg - var(--a) / 4)) 100%,
calc(var(--_p) + var(--h) * tan(var(--a) / 2) - var(--b) * tan(45deg - var(--a) / 4))
calc(100% - var(--b)),
calc(var(--_p) - var(--h) * tan(var(--a) / 2) + var(--b) * tan(45deg - var(--a) / 4))
calc(100% - var(--b)),
calc(var(--_p) - var(--h) * tan(var(--a) / 2) + var(--b) * tan(45deg - var(--a) / 4)) 100%
);
-webkit-mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
}
.tooltip:after {
bottom: calc(-1 * var(--h));
clip-path: polygon(
calc(var(--_p) + var(--h) * tan(var(--a) / 2)) calc(100% - var(--h)),
var(--_p) 100%,
calc(var(--_p) - var(--h) * tan(var(--a) / 2)) calc(100% - var(--h)),
calc(var(--_p) - var(--h) * tan(var(--a) / 2) + var(--b) * tan(45deg - var(--a) / 4))
calc(100% - var(--h) - var(--b)),
var(--_p) calc(100% - var(--b) / sin(var(--a) / 2)),
calc(var(--_p) + var(--h) * tan(var(--a) / 2) - var(--b) * tan(45deg - var(--a) / 4))
calc(100% - var(--h) - var(--b))
);
}
But setting a semi-transparent background color (and a border-radius to hide the overflow behind the border) doesn't fill in the little triangle at the bottom. Nor could I find a way to do so.
I also found a related question here which suggests an approach which I adapted to the code below
<div class="background">
<div class="bubble">
<p>
Hello world! I am some content. Who's got time for lorems?
</p>
</div>
</div>
.background {
background: pink;
display: flex;
width: 100%;
height: 100vh;
}
.bubble {
height: fit-content;
color: red;
max-width: 75%;
min-width: 200px;
padding: 8px 12px 0 12px;
position: relative;
border-radius: 0 0 4px 4px;
margin-top:40px;
border: 1px solid red;
border-top:0;
margin-right: 16px;
background: #0000bb55;
}
.bubble::before,
.bubble::after{
content: '';
position: absolute;
width: 70%;
height: 20px;
top: -21px;
border:1px solid;
background: #0000bb55;
}
.bubble::before {
right:-1px;
border:1px solid;
border-width:1px 1px 0 0;
transform-origin:bottom;
transform:skew(-45deg);
}
.bubble::after{
left:-1px;
border-width:1px 0 0 1px;
border-radius:4px 0 0 0;
}
p {
margin-top:0;
}
But as soon as you start introducing semi-transparent colors, those skewed transforms start overlapping and introducing color discrepancies.
Finally, the answers in this post also seems to have different opacity levels for the triangle and the background:
<div class="wrapper">
<p class="infoBubble">aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</p>
</div>
.wrapper {
background: pink;
height: 500px;
width: 100vw;
}
.infoBubble {
font-family:arial;
font-size:12px;
color:#FFFFFF;
display: inline-block;
position: absolute;
padding:6px;
background-color: rgba(0, 0, 0, 0.7);
-webkit-border-radius:7px;
-moz-border-radius:7px;
border-radius:7px;
}
.infoBubble:after {
content:"";
position:absolute;
bottom:-6px;
left:20px;
opacity:0.7;
border-width:6px 6px 0;
border-style:solid;
border-color: rgba(0, 0, 0, 0.7) transparent;
display:block;
width:0;
}
So, I am wondering.. what is the right approach here?
It seems my collection doesn't contain that case but we can achieve it by combining #37 with #21 https://css-generators.com/tooltip-speech-bubble/
First you copy the code of #37, then you add an extra span that will contain the code of #21 (not all its code but only the pseudo element part)
.tooltip {
/* triangle dimension */
--a: 90deg; /* angle */
--h: 1em; /* height */
--p: 50%; /* triangle position (0%:left 100%:right) */
--b: 7px; /* border width */
--r: 1.2em; /* the radius */
--c1: #0009; /* semi-transparent background*/
--c2: red; /* border color*/
padding: 1em;
color: #fff;
position: relative;
z-index: 0;
}
.tooltip:before,
.tooltip:after {
content: "";
position: absolute;
z-index: -1;
inset: 0;
background: conic-gradient(var(--c2) 0 0);
--_p:clamp(var(--h)*tan(var(--a)/2) + var(--b),var(--p),100% - var(--h)*tan(var(--a)/2) - var(--b));
}
.tooltip:before {
padding: var(--b);
border-radius: var(--r) var(--r) min(var(--r),100% - var(--p) - var(--h)*tan(var(--a)/2)) min(var(--r),var(--p) - var(--h)*tan(var(--a)/2))/var(--r);
background-size: 100% calc(100% + var(--h));
clip-path: polygon(0 100%,0 0,100% 0,100% 100%,
calc(var(--_p) + var(--h)*tan(var(--a)/2) - var(--b)*tan(45deg - var(--a)/4)) 100%,
calc(var(--_p) + var(--h)*tan(var(--a)/2) - var(--b)*tan(45deg - var(--a)/4)) calc(100% - var(--b)),
calc(var(--_p) - var(--h)*tan(var(--a)/2) + var(--b)*tan(45deg - var(--a)/4)) calc(100% - var(--b)),
calc(var(--_p) - var(--h)*tan(var(--a)/2) + var(--b)*tan(45deg - var(--a)/4)) 100%
);
-webkit-mask: linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
}
.tooltip:after {
bottom: calc(-1*var(--h));
clip-path: polygon(
calc(var(--_p) + var(--h)*tan(var(--a)/2)) calc(100% - var(--h)),
var(--_p) 100%,
calc(var(--_p) - var(--h)*tan(var(--a)/2)) calc(100% - var(--h)),
calc(var(--_p) - var(--h)*tan(var(--a)/2) + var(--b)*tan(45deg - var(--a)/4)) calc(100% - var(--h) - var(--b)),
var(--_p) calc(100% - var(--b)/sin(var(--a)/2)),
calc(var(--_p) + var(--h)*tan(var(--a)/2) - var(--b)*tan(45deg - var(--a)/4)) calc(100% - var(--h) - var(--b)));
}
.tooltip span {
position: absolute;
z-index: -1;
inset: 0;
padding: var(--b);
border-radius: var(--r);
clip-path: polygon(0 100%, 0 0, 100% 0, 100% 100%, min(100% - var(--b), var(--p) + var(--h)* tan(var(--a) / 2) - var(--b)* tan(45deg - var(--a) / 4)) calc(100% - var(--b)), var(--p) calc(100% + var(--h) - var(--b) / sin(var(--a) / 2)), max(var(--b), var(--p) - var(--h)* tan(var(--a) / 2) + var(--b)* tan(45deg - var(--a) / 4)) calc(100% - var(--b)));
background: var(--c1) content-box;
border-image: conic-gradient(var(--c1) 0 0) fill 0 / calc(100% - var(--h) - var(--b)) max(var(--b), 100% - var(--p) - var(--h)* tan(var(--a) / 2)) 0 max(var(--b), var(--p) - var(--h)* tan(var(--a) / 2)) / 0 0 var(--h) 0;
}
body {
background: url(https://picsum.photos/id/107/800/600) center/cover
}
.tooltip {
font-size: 18px;
max-width: 28ch;
text-align: center;
}
<div class="tooltip">
<span></span>
This is a Tooltip with a border and a border radius. Border can have a solid or gradient coloration and the background is transparent</div>