diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt
index bbf7890da1..d059eb83fc 100644
--- a/Source/Core/DolphinQt/CMakeLists.txt
+++ b/Source/Core/DolphinQt/CMakeLists.txt
@@ -81,6 +81,8 @@ add_executable(dolphin-emu
   Config/GeckoCodeWidget.h
   Config/Graphics/AdvancedWidget.cpp
   Config/Graphics/AdvancedWidget.h
+  Config/Graphics/BalloonTip.cpp
+  Config/Graphics/BalloonTip.h
   Config/Graphics/EnhancementsWidget.cpp
   Config/Graphics/EnhancementsWidget.h
   Config/Graphics/GeneralWidget.cpp
diff --git a/Source/Core/DolphinQt/Config/Graphics/BalloonTip.cpp b/Source/Core/DolphinQt/Config/Graphics/BalloonTip.cpp
new file mode 100644
index 0000000000..6be2c637f0
--- /dev/null
+++ b/Source/Core/DolphinQt/Config/Graphics/BalloonTip.cpp
@@ -0,0 +1,269 @@
+// Copyright 2020 Dolphin Emulator Project
+// Licensed under GPLv2+
+// Refer to the license.txt file included.
+
+#include "DolphinQt/Config/Graphics/BalloonTip.h"
+
+#include <memory>
+
+#include <QBitmap>
+#include <QGraphicsEffect>
+#include <QGraphicsView>
+#include <QGridLayout>
+#include <QGuiApplication>
+#include <QLabel>
+#include <QPainter>
+#include <QPainterPath>
+#include <QPropertyAnimation>
+#include <QPushButton>
+#include <QStyle>
+
+#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
+#include <QScreen>
+#else
+#include <QApplication>
+#include <QDesktopWidget>
+#endif
+
+#if defined(__APPLE__)
+#include <QToolTip>
+#endif
+
+namespace
+{
+std::unique_ptr<BalloonTip> s_the_balloon_tip = nullptr;
+}  // namespace
+
+void BalloonTip::ShowBalloon(const QIcon& icon, const QString& title, const QString& message,
+                             const QPoint& pos, QWidget* parent, ShowArrow show_arrow)
+{
+  HideBalloon();
+  if (message.isEmpty() && title.isEmpty())
+    return;
+
+#if defined(__APPLE__)
+  QString the_message = message;
+  the_message.replace(QStringLiteral("<dolphin_emphasis>"), QStringLiteral("<b>"));
+  the_message.replace(QStringLiteral("</dolphin_emphasis>"), QStringLiteral("</b>"));
+  QToolTip::showText(pos, the_message, parent);
+#else
+  s_the_balloon_tip = std::make_unique<BalloonTip>(PrivateTag{}, icon, title, message, parent);
+  s_the_balloon_tip->UpdateBoundsAndRedraw(pos, show_arrow);
+#endif
+}
+
+void BalloonTip::HideBalloon()
+{
+#if defined(__APPLE__)
+  QToolTip::hideText();
+#else
+  if (!s_the_balloon_tip)
+    return;
+  s_the_balloon_tip->hide();
+  s_the_balloon_tip.reset();
+#endif
+}
+
+BalloonTip::BalloonTip(PrivateTag, const QIcon& icon, QString title, QString message,
+                       QWidget* parent)
+    : QWidget(nullptr, Qt::ToolTip)
+{
+  setAttribute(Qt::WA_DeleteOnClose);
+  setAutoFillBackground(true);
+
+  const QPalette& pal = parent->palette();
+
+  const auto theme_window_color = pal.color(QPalette::Base);
+  const auto theme_window_hsv = theme_window_color.toHsv();
+
+  const auto brightness = theme_window_hsv.value();
+
+  QColor window_color;
+  QColor text_color;
+  QColor dolphin_emphasis;
+  if (brightness > 128)
+  {
+    // Our theme color is light, so make it darker
+    window_color = QColor(72, 72, 72);
+    text_color = Qt::white;
+    dolphin_emphasis = Qt::yellow;
+    m_border_color = palette().color(QPalette::Window).darker(160);
+  }
+  else
+  {
+    // Our theme color is dark, so make it lighter
+    window_color = Qt::white;
+    text_color = Qt::black;
+    dolphin_emphasis = QColor(QStringLiteral("#0090ff"));
+    m_border_color = palette().color(QPalette::Window).darker(160);
+  }
+
+  const auto style_sheet = QStringLiteral("background-color: #%1; color: #%2;")
+                               .arg(window_color.rgba(), 0, 16)
+                               .arg(text_color.rgba(), 0, 16);
+  setStyleSheet(style_sheet);
+
+  // Replace text in our our message
+  // if specific "tags" are used
+  message.replace(QStringLiteral("<dolphin_emphasis>"),
+                  QStringLiteral("<font color=\"#%1\"><b>").arg(dolphin_emphasis.rgba(), 0, 16));
+  message.replace(QStringLiteral("</dolphin_emphasis>"), QStringLiteral("</b></font>"));
+
+  auto* title_label = new QLabel;
+  title_label->installEventFilter(this);
+  title_label->setText(title);
+  QFont f = title_label->font();
+  f.setBold(true);
+  title_label->setFont(f);
+  title_label->setTextFormat(Qt::RichText);
+  title_label->setSizePolicy(QSizePolicy::Policy::MinimumExpanding,
+                             QSizePolicy::Policy::MinimumExpanding);
+
+  auto* message_label = new QLabel;
+  message_label->installEventFilter(this);
+  message_label->setText(message);
+  message_label->setTextFormat(Qt::RichText);
+  message_label->setAlignment(Qt::AlignTop | Qt::AlignLeft);
+
+#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
+  const int limit = QApplication::desktop()->availableGeometry(message_label).width() / 3;
+#else
+  const int limit = message_label->screen()->availableGeometry().width() / 3;
+#endif
+  message_label->setMaximumWidth(limit);
+  message_label->setSizePolicy(QSizePolicy::Policy::MinimumExpanding,
+                               QSizePolicy::Policy::MinimumExpanding);
+  if (message_label->sizeHint().width() > limit)
+  {
+    message_label->setWordWrap(true);
+  }
+
+  auto* layout = new QGridLayout;
+  layout->addWidget(title_label, 0, 0, 1, 2);
+
+  layout->addWidget(message_label, 1, 0, 1, 3);
+  layout->setSizeConstraint(QLayout::SetMinimumSize);
+  setLayout(layout);
+}
+
+void BalloonTip::paintEvent(QPaintEvent*)
+{
+  QPainter painter(this);
+  painter.drawPixmap(rect(), m_pixmap);
+}
+
+void BalloonTip::UpdateBoundsAndRedraw(const QPoint& pos, ShowArrow show_arrow)
+{
+  m_show_arrow = show_arrow == ShowArrow::Yes;
+
+#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
+  const QRect screen_rect = QApplication::desktop()->screenGeometry(pos);
+#else
+  QScreen* screen = QGuiApplication::screenAt(pos);
+  if (!screen)
+    screen = QGuiApplication::primaryScreen();
+  const QRect screen_rect = screen->geometry();
+#endif
+  QSize sh = sizeHint();
+  const int border = 1;
+  const int arrow_height = 18;
+  const int arrow_width = 18;
+  const int arrow_offset = 52;
+  const int rect_center = 7;
+  const bool arrow_at_bottom = (pos.y() - sh.height() - arrow_height > 0);
+  const bool arrow_at_left = (pos.x() + sh.width() - arrow_width < screen_rect.width());
+  const int default_padding = 10;
+  layout()->setContentsMargins(border + 3 + default_padding,
+                               border + (arrow_at_bottom ? 0 : arrow_height) + 2 + default_padding,
+                               border + 3 + default_padding,
+                               border + (arrow_at_bottom ? arrow_height : 0) + 2 + default_padding);
+  updateGeometry();
+  sh = sizeHint();
+
+  int ml, mr, mt, mb;
+  QSize sz = sizeHint();
+  if (arrow_at_bottom)
+  {
+    ml = mt = 0;
+    mr = sz.width() - 1;
+    mb = sz.height() - arrow_height - 1;
+  }
+  else
+  {
+    ml = 0;
+    mt = arrow_height;
+    mr = sz.width() - 1;
+    mb = sz.height() - 1;
+  }
+
+  QPainterPath path;
+  path.moveTo(ml + rect_center, mt);
+  if (!arrow_at_bottom && arrow_at_left)
+  {
+    if (m_show_arrow)
+    {
+      path.lineTo(ml + arrow_offset - arrow_width, mt);
+      path.lineTo(ml + arrow_offset, mt - arrow_height);
+      path.lineTo(ml + arrow_offset + arrow_width, mt);
+    }
+    move(qMax(pos.x() - arrow_offset, screen_rect.left() + 2), pos.y());
+  }
+  else if (!arrow_at_bottom && !arrow_at_left)
+  {
+    if (m_show_arrow)
+    {
+      path.lineTo(mr - arrow_offset - arrow_width, mt);
+      path.lineTo(mr - arrow_offset, mt - arrow_height);
+      path.lineTo(mr - arrow_offset + arrow_width, mt);
+    }
+    move(qMin(pos.x() - sh.width() + arrow_offset, screen_rect.right() - sh.width() - 2), pos.y());
+  }
+  path.lineTo(mr - rect_center, mt);
+  path.arcTo(QRect(mr - rect_center * 2, mt, rect_center * 2, rect_center * 2), 90, -90);
+  path.lineTo(mr, mb - rect_center);
+  path.arcTo(QRect(mr - rect_center * 2, mb - rect_center * 2, rect_center * 2, rect_center * 2), 0,
+             -90);
+  if (arrow_at_bottom && !arrow_at_left)
+  {
+    if (m_show_arrow)
+    {
+      path.lineTo(mr - arrow_offset + arrow_width, mb);
+      path.lineTo(mr - arrow_offset, mb + arrow_height);
+      path.lineTo(mr - arrow_offset - arrow_width, mb);
+    }
+    move(qMin(pos.x() - sh.width() + arrow_offset, screen_rect.right() - sh.width() - 2),
+         pos.y() - sh.height());
+  }
+  else if (arrow_at_bottom && arrow_at_left)
+  {
+    if (m_show_arrow)
+    {
+      path.lineTo(arrow_offset + arrow_width, mb);
+      path.lineTo(arrow_offset, mb + arrow_height);
+      path.lineTo(arrow_offset - arrow_width, mb);
+    }
+    move(qMax(pos.x() - arrow_offset, screen_rect.x() + 2), pos.y() - sh.height());
+  }
+  path.lineTo(ml + rect_center, mb);
+  path.arcTo(QRect(ml, mb - rect_center * 2, rect_center * 2, rect_center * 2), -90, -90);
+  path.lineTo(ml, mt + rect_center);
+  path.arcTo(QRect(ml, mt, rect_center * 2, rect_center * 2), 180, -90);
+
+  // Set the mask
+  QBitmap bitmap(sizeHint());
+  bitmap.fill(Qt::color0);
+  QPainter painter1(&bitmap);
+  painter1.setPen(QPen(Qt::color1, border));
+  painter1.setBrush(QBrush(Qt::color1));
+  painter1.drawPath(path);
+  setMask(bitmap);
+
+  // Draw the border
+  m_pixmap = QPixmap(sz);
+  QPainter painter2(&m_pixmap);
+  painter2.setPen(QPen(m_border_color));
+  painter2.setBrush(palette().color(QPalette::Window));
+  painter2.drawPath(path);
+
+  show();
+}
diff --git a/Source/Core/DolphinQt/Config/Graphics/BalloonTip.h b/Source/Core/DolphinQt/Config/Graphics/BalloonTip.h
new file mode 100644
index 0000000000..0bfb013f3c
--- /dev/null
+++ b/Source/Core/DolphinQt/Config/Graphics/BalloonTip.h
@@ -0,0 +1,42 @@
+// Copyright 2020 Dolphin Emulator Project
+// Licensed under GPLv2+
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <QIcon>
+#include <QPixmap>
+#include <QWidget>
+
+class BalloonTip : public QWidget
+{
+  Q_OBJECT
+
+  struct PrivateTag
+  {
+  };
+
+public:
+  enum class ShowArrow
+  {
+    Yes,
+    No
+  };
+  static void ShowBalloon(const QIcon& icon, const QString& title, const QString& msg,
+                          const QPoint& pos, QWidget* parent,
+                          ShowArrow show_arrow = ShowArrow::Yes);
+  static void HideBalloon();
+
+  BalloonTip(PrivateTag, const QIcon& icon, QString title, QString msg, QWidget* parent);
+
+private:
+  void UpdateBoundsAndRedraw(const QPoint&, ShowArrow);
+
+protected:
+  void paintEvent(QPaintEvent*) override;
+
+private:
+  QColor m_border_color;
+  QPixmap m_pixmap;
+  bool m_show_arrow = true;
+};
diff --git a/Source/Core/DolphinQt/DolphinQt.vcxproj b/Source/Core/DolphinQt/DolphinQt.vcxproj
index 04f86f2015..56edeccf7a 100644
--- a/Source/Core/DolphinQt/DolphinQt.vcxproj
+++ b/Source/Core/DolphinQt/DolphinQt.vcxproj
@@ -60,6 +60,7 @@
     <ClCompile Include="Config\GameConfigWidget.cpp" />
     <ClCompile Include="Config\GeckoCodeWidget.cpp" />
     <ClCompile Include="Config\Graphics\AdvancedWidget.cpp" />
+    <ClCompile Include="Config\Graphics\BalloonTip.cpp" />
     <ClCompile Include="Config\Graphics\EnhancementsWidget.cpp" />
     <ClCompile Include="Config\Graphics\GeneralWidget.cpp" />
     <ClCompile Include="Config\Graphics\GraphicsBool.cpp" />
@@ -222,6 +223,7 @@
     <QtMoc Include="Config\GameConfigWidget.h" />
     <QtMoc Include="Config\GeckoCodeWidget.h" />
     <QtMoc Include="Config\Graphics\AdvancedWidget.h" />
+    <QtMoc Include="Config\Graphics\BalloonTip.h" />
     <QtMoc Include="Config\Graphics\EnhancementsWidget.h" />
     <QtMoc Include="Config\Graphics\GeneralWidget.h" />
     <QtMoc Include="Config\Graphics\GraphicsBool.h" />