diff --git a/src/vte.cc b/src/vte.cc
index 609d4e2..e58c653 100644
--- a/src/vte.cc
+++ b/src/vte.cc
@@ -437,6 +437,12 @@ VteTerminalPrivate::invalidate_all()
 	_vte_debug_print (VTE_DEBUG_WORK, "*");
 	_vte_debug_print (VTE_DEBUG_UPDATES, "Invalidating all.\n");
 
+        /* this sometimes saves us a full repaint after blinking has stopped */
+        if (G_UNLIKELY (m_contents_blink_tag != 0)) {
+                g_source_remove(m_contents_blink_tag);
+                m_contents_blink_tag = 0;
+        }
+
 	/* replace invalid regions with one covering the whole terminal */
 	reset_update_rects();
 	m_invalidated_all = TRUE;
@@ -7803,6 +7809,7 @@ VteTerminalPrivate::VteTerminalPrivate(VteTerminal *t) :
 	m_pending = g_array_new(FALSE, TRUE, sizeof(gunichar));
 	m_max_input_bytes = VTE_MAX_INPUT_READ;
 	m_cursor_blink_tag = 0;
+        m_contents_blink_tag = 0;
 	m_outgoing = _vte_byte_array_new();
 	m_outgoing_conv = VTE_INVALID_CONV;
 	m_conv_buffer = _vte_byte_array_new();
@@ -8109,7 +8116,7 @@ void
 VteTerminalPrivate::widget_settings_notify()
 {
         gboolean blink;
-        int blink_time = 1000;
+        int blink_time = VTE_BLINK_TIME_DEFAULT;
         int blink_timeout = G_MAXINT;
 
         g_object_get(gtk_widget_get_settings(m_widget),
@@ -8509,6 +8516,9 @@ VteTerminalPrivate::determine_colors(VteCellAttr const* attr,
 	}
 
 	/* Invisible? */
+        /* Note: This is not where we actually handle invisibile.
+         * Instead, draw_cells() is not called from draw_rows().
+         * That is required for the foreground to be transparent if so is the background. */
 	if (attr->invisible) {
 		fore = back;
 	}
@@ -8539,6 +8549,14 @@ VteTerminalPrivate::determine_cursor_colors(VteCell const* cell,
                          fore, back);
 }
 
+static gboolean
+invalidate_all_cb(VteTerminalPrivate *that)
+{
+        that->m_contents_blink_tag = 0;
+        that->invalidate_all();
+        return G_SOURCE_REMOVE;
+}
+
 /* Draw a string of characters with similar attributes. */
 void
 VteTerminalPrivate::draw_cells(struct _vte_draw_text_request *items,
@@ -8551,6 +8569,7 @@ VteTerminalPrivate::draw_cells(struct _vte_draw_text_request *items,
                                bool italic,
                                bool underline,
                                bool strikethrough,
+                               bool blink,
                                bool hilite,
                                bool boxed,
                                int column_width,
@@ -8559,6 +8578,7 @@ VteTerminalPrivate::draw_cells(struct _vte_draw_text_request *items,
 	int i, x, y;
 	gint columns = 0;
 	vte::color::rgb fg, bg;
+	bool skip_drawing_text = FALSE;
 
 	g_assert(n > 0);
 	_VTE_DEBUG_IF(VTE_DEBUG_CELLS) {
@@ -8569,9 +8589,9 @@ VteTerminalPrivate::draw_cells(struct _vte_draw_text_request *items,
 		}
 		tmp = g_string_free (str, FALSE);
 		g_printerr ("draw_cells('%s', fore=%d, back=%d, bold=%d,"
-				" ul=%d, strike=%d, hilite=%d, boxed=%d)\n",
+				" ul=%d, strike=%d, blink=%d, hilite=%d, boxed=%d)\n",
 				tmp, fore, back, bold,
-				underline, strikethrough, hilite, boxed);
+				underline, strikethrough, blink, hilite, boxed);
 		g_free (tmp);
 	}
 
@@ -8579,6 +8599,26 @@ VteTerminalPrivate::draw_cells(struct _vte_draw_text_request *items,
 	rgb_from_index(fore, fg);
 	rgb_from_index(back, bg);
 
+        if (blink) {
+                unsigned int blink_time = VTE_BLINK_TIME_DEFAULT;
+                guint64 now;
+
+                g_object_get(gtk_widget_get_settings(m_widget),
+                        "gtk-cursor-blink-time", &blink_time,
+                        nullptr);
+                now = g_get_monotonic_time() / 1000;
+
+                if (now % blink_time >= blink_time / 2)
+                        skip_drawing_text = TRUE;
+                /* TODO: We could go for a bounding box of blinking text instead of forcing a full repaint. */
+                if (m_contents_blink_tag == 0)
+                        m_contents_blink_tag = g_timeout_add_full(G_PRIORITY_LOW,
+                                                                  blink_time / 2 - now % (blink_time / 2),
+                                                                  (GSourceFunc)invalidate_all_cb,
+                                                                  this,
+                                                                  NULL);
+        }
+
 	i = 0;
 	do {
 		columns = 0;
@@ -8598,6 +8638,14 @@ VteTerminalPrivate::draw_cells(struct _vte_draw_text_request *items,
 		}
 	} while (i < n);
 
+        /* This is for the "off" state of blinking text. Invisible text could also be handled here,
+         * but it's not, it's actually handled outside by not even calling this method.
+         * Setting fg = bg and painting the text would not work for two reasons: it'd be opaque
+         * even if the background is translucent, and this method can be called with a continuous
+         * run of identical fg, yet different bg colored cells. So we must bail out. */
+        if (skip_drawing_text)
+                return;
+
 	_vte_draw_text(m_draw,
 			items, n,
 			&fg, VTE_DRAW_OPAQUE,
@@ -8869,6 +8917,7 @@ VteTerminalPrivate::draw_cells_with_attributes(struct _vte_draw_text_request *it
 					cells[j].attr.italic,
 					cells[j].attr.underline,
 					cells[j].attr.strikethrough,
+                                        cells[j].attr.blink,
 					FALSE, FALSE, column_width, height);
 		j += g_unichar_to_utf8(items[i].c, scratch_buf);
 	}
@@ -8896,7 +8945,7 @@ VteTerminalPrivate::draw_rows(VteScreen *screen_,
         long x, y;
 	guint fore, nfore, back, nback;
 	gboolean underline, nunderline, bold, nbold, italic, nitalic, hilite, nhilite,
-		 selected, nselected, strikethrough, nstrikethrough;
+		 selected, nselected, strikethrough, nstrikethrough, blink, nblink;
 	guint item_count;
 	const VteCell *cell;
 	VteRowData const* row_data;
@@ -9047,6 +9096,7 @@ VteTerminalPrivate::draw_rows(VteScreen *screen_,
 			strikethrough = cell->attr.strikethrough;
 			bold = cell->attr.bold;
 			italic = cell->attr.italic;
+			blink = cell->attr.blink;
 			if (m_show_match) {
 				hilite = m_match_span.contains(row, i);
 			} else {
@@ -9111,6 +9161,10 @@ VteTerminalPrivate::draw_rows(VteScreen *screen_,
 					if (nstrikethrough != strikethrough) {
 						break;
 					}
+                                        nblink = cell->attr.blink;
+                                        if (nblink != blink) {
+                                                break;
+                                        }
 					/* Break up matched/not-matched text. */
 					nhilite = false;
 					if (m_show_match) {
@@ -9162,7 +9216,7 @@ fg_draw:
 					item_count,
 					fore, back, FALSE, FALSE,
 					bold, italic, underline,
-					strikethrough, hilite, FALSE,
+					strikethrough, blink, hilite, FALSE,
 					column_width, row_height);
 			item_count = 1;
 			/* We'll need to continue at the first cell which didn't
@@ -9367,6 +9421,7 @@ VteTerminalPrivate::paint_cursor()
                                                         cell->attr.italic,
                                                         cell->attr.underline,
                                                         cell->attr.strikethrough,
+                                                        cell->attr.blink,
                                                         FALSE,
                                                         FALSE,
                                                         width,
@@ -9452,6 +9507,7 @@ VteTerminalPrivate::paint_im_preedit_string()
 						FALSE,
 						FALSE,
 						FALSE,
+                                                FALSE,
 						FALSE,
 						TRUE,
 						width, height);
diff --git a/src/vtedefines.hh b/src/vtedefines.hh
index 7f56291..7aa3f55 100644
--- a/src/vtedefines.hh
+++ b/src/vtedefines.hh
@@ -82,6 +82,7 @@
 #define VTE_UPDATE_TIMEOUT		15
 #define VTE_UPDATE_REPEAT_TIMEOUT	30
 #define VTE_MAX_PROCESS_TIME		100
+#define VTE_BLINK_TIME_DEFAULT          1000
 #define VTE_CELL_BBOX_SLACK		1
 #define VTE_DEFAULT_UTF8_AMBIGUOUS_WIDTH 1
 
diff --git a/src/vteinternal.hh b/src/vteinternal.hh
index cff2157..8597928 100644
--- a/src/vteinternal.hh
+++ b/src/vteinternal.hh
@@ -406,6 +406,8 @@ public:
         gboolean m_cursor_visible;
         gboolean m_has_focus;               /* is the terminal window focused */
 
+        guint m_contents_blink_tag;
+
         /* DECSCUSR cursor style (shape and blinking possibly overridden
          * via escape sequence) */
         VteCursorStyle m_cursor_style;
@@ -689,6 +691,7 @@ public:
                         bool italic,
                         bool underline,
                         bool strikethrough,
+                        bool blink,
                         bool hilite,
                         bool boxed,
                         int column_width,
