Contextual callout pointing at a UI element.
<div class="coachmark-stage">
<button type="button" class="coachmark-stage__target coachmark-stage__target--pulse" style="--accent:#A8E6C1">Export button</button>
<div class="coachmark" data-placement="bottom" style="--accent:#A8E6C1" role="dialog" aria-label="Tip">
<button type="button" class="coachmark__dismiss" aria-label="Dismiss coachmark">×</button>
<p class="coachmark__message">Export your report as CSV or PDF from here.</p>
<span class="coachmark__arrow" aria-hidden="true"></span>
</div>
</div>
<style>
.coachmark-stage { position: relative; font: 16px/1.5 system-ui, sans-serif; min-height: 190px; padding: 60px 24px; display: flex; align-items: center; justify-content: center; background: #FAF7F2; border-radius: 12px; }
.coachmark-stage__target { font: 600 14px system-ui, sans-serif; padding: 10px 18px; border-radius: 8px; border: 1.5px solid #24143030; background: #fff; color: #241430; cursor: default; position: relative; }
.coachmark-stage__target--pulse::after { content: ""; position: absolute; inset: -6px; border-radius: 12px; border: 2px solid var(--accent); }
@media (prefers-reduced-motion: no-preference) {
.coachmark-stage__target--pulse::after { animation: coachmark-pulse .22s ease-out infinite; }
}
@keyframes coachmark-pulse { 0% { opacity: .9; transform: scale(.94); } 100% { opacity: 0; transform: scale(1.18); } }
.coachmark { position: absolute; width: 220px; background: #241430; color: #FAF7F2; padding: 14px 16px; border-radius: 10px; box-shadow: 0 12px 28px rgba(36,20,48,.28); }
.coachmark[data-placement="bottom"] { top: calc(50% + 46px); left: 50%; transform: translateX(-50%); }
.coachmark[data-placement="top"] { bottom: calc(50% + 46px); left: 50%; transform: translateX(-50%); top: auto; }
.coachmark[data-placement="right"] { left: calc(50% + 92px); top: 50%; transform: translateY(-50%); }
.coachmark[data-placement="left"] { right: calc(50% + 92px); top: 50%; transform: translateY(-50%); left: auto; }
.coachmark__dismiss { position: absolute; top: 6px; right: 8px; background: transparent; border: none; color: #FAF7F2cc; font-size: 16px; line-height: 1; cursor: pointer; padding: 4px; }
.coachmark__dismiss:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-radius: 4px; }
.coachmark__message { margin: 4px 18px 0 0; font-size: 13.5px; }
.coachmark__arrow { position: absolute; width: 10px; height: 10px; background: #241430; }
.coachmark[data-placement="bottom"] .coachmark__arrow { top: -4px; left: 50%; margin-left: -5px; transform: rotate(45deg); }
.coachmark[data-placement="top"] .coachmark__arrow { bottom: -4px; left: 50%; margin-left: -5px; transform: rotate(45deg); }
.coachmark[data-placement="right"] .coachmark__arrow { left: -4px; top: 50%; margin-top: -5px; transform: rotate(45deg); }
.coachmark[data-placement="left"] .coachmark__arrow { right: -4px; top: 50%; margin-top: -5px; transform: rotate(45deg); }
</style><div class="relative font-sans min-h-[190px] px-6 py-14 flex items-center justify-center bg-[#FAF7F2] rounded-xl">
<button type="button" class="coachmark-tw-target coachmark-tw-target--pulse relative font-semibold text-sm px-4.5 py-2.5 rounded-lg border-[1.5px] border-[#24143030] bg-white text-[#241430]" style="--accent:#A8E6C1">Export button</button>
<div class="coachmark-tw absolute w-[220px] bg-[#241430] text-[#FAF7F2] p-3.5 rounded-[10px] shadow-[0_12px_28px_rgba(36,20,48,.28)]" data-placement="bottom" role="dialog" aria-label="Tip">
<button type="button" class="absolute top-1.5 right-2 bg-transparent border-0 text-base leading-none cursor-pointer p-1" aria-label="Dismiss coachmark">×</button>
<p class="mt-1 mr-4.5 mb-0 text-[13.5px]">Export your report as CSV or PDF from here.</p>
</div>
</div>
<style>
.coachmark-tw-target--pulse::after { content: ""; position: absolute; inset: -6px; border-radius: 12px; border: 2px solid var(--accent); }
@media (prefers-reduced-motion: no-preference) {
.coachmark-tw-target--pulse::after { animation: coachmark-tw-pulse .22s ease-out infinite; }
}
@keyframes coachmark-tw-pulse { 0% { opacity: .9; transform: scale(.94); } 100% { opacity: 0; transform: scale(1.18); } }
.coachmark-tw[data-placement="bottom"] { top: calc(50% + 46px); left: 50%; transform: translateX(-50%); }
.coachmark-tw[data-placement="top"] { bottom: calc(50% + 46px); left: 50%; transform: translateX(-50%); top: auto; }
.coachmark-tw[data-placement="right"] { left: calc(50% + 92px); top: 50%; transform: translateY(-50%); }
.coachmark-tw[data-placement="left"] { right: calc(50% + 92px); top: 50%; transform: translateY(-50%); left: auto; }
</style>import { useState } from "react";
export function Coachmark({
targetLabel = "Export button",
message = "Export your report as CSV or PDF from here.",
placement = "bottom",
accent = "#A8E6C1",
pulse = true,
}) {
const [visible, setVisible] = useState(true);
const pos = {
bottom: { top: "calc(50% + 46px)", left: "50%", transform: "translateX(-50%)" },
top: { bottom: "calc(50% + 46px)", left: "50%", transform: "translateX(-50%)" },
right: { left: "calc(50% + 92px)", top: "50%", transform: "translateY(-50%)" },
left: { right: "calc(50% + 92px)", top: "50%", transform: "translateY(-50%)" },
}[placement] ?? {};
return (
<div style={{ position: "relative", fontFamily: "system-ui, sans-serif", minHeight: 190, padding: "60px 24px", display: "flex", alignItems: "center", justifyContent: "center", background: "#FAF7F2", borderRadius: 12 }}>
<button type="button" style={{ position: "relative", font: "600 14px system-ui, sans-serif", padding: "10px 18px", borderRadius: 8, border: "1.5px solid #24143030", background: "#fff", color: "#241430" }}>
{targetLabel}
{pulse && (
<span style={{ position: "absolute", inset: -6, borderRadius: 12, border: "2px solid " + accent, pointerEvents: "none" }} />
)}
</button>
{visible && (
<div role="dialog" aria-label="Tip" style={{ position: "absolute", width: 220, background: "#241430", color: "#FAF7F2", padding: "14px 16px", borderRadius: 10, boxShadow: "0 12px 28px rgba(36,20,48,.28)", ...pos }}>
<button type="button" aria-label="Dismiss coachmark" onClick={() => setVisible(false)} style={{ position: "absolute", top: 6, right: 8, background: "transparent", border: "none", color: "#FAF7F2cc", fontSize: 16, lineHeight: 1, cursor: "pointer", padding: 4 }}>
×
</button>
<p style={{ margin: "4px 18px 0 0", fontSize: 13.5 }}>{message}</p>
</div>
)}
</div>
);
}