diff --git a/doc/reference/vte-sections.txt b/doc/reference/vte-sections.txt
index b87d249f..2e6016b1 100644
--- a/doc/reference/vte-sections.txt
+++ b/doc/reference/vte-sections.txt
@@ -23,6 +23,8 @@ vte_terminal_set_font_scale
 vte_terminal_get_font_scale
 vte_terminal_set_audible_bell
 vte_terminal_get_audible_bell
+vte_terminal_set_allow_blink
+vte_terminal_get_allow_blink
 vte_terminal_set_allow_bold
 vte_terminal_get_allow_bold
 vte_terminal_set_bold_is_bright
@@ -107,6 +109,8 @@ vte_terminal_watch_child
 vte_terminal_set_background_operator
 
 <SUBSECTION Standard>
+VTE_TYPE_ALLOW_BLINK
+vte_blink_get_type
 VTE_TYPE_CURSOR_BLINK_MODE
 vte_cursor_blink_mode_get_type
 VTE_TYPE_CURSOR_SHAPE
diff --git a/src/app/app.cc b/src/app/app.cc
index 636df478..f5e11a1f 100644
--- a/src/app/app.cc
+++ b/src/app/app.cc
@@ -91,6 +91,7 @@ public:
         int verbosity{0};
         double cell_height_scale{1.0};
         double cell_width_scale{1.0};
+        VteBlink blink_mode{VTE_BLINK_ALWAYS};
         VteCursorBlinkMode cursor_blink_mode{VTE_CURSOR_BLINK_SYSTEM};
         VteCursorShape cursor_shape{VTE_CURSOR_SHAPE_BLOCK};
 
@@ -214,6 +215,17 @@ private:
                 return rv;
         }
 
+        static gboolean
+        parse_blink(char const* option, char const* value, void* data, GError** error)
+        {
+                Options* that = static_cast<Options*>(data);
+                int v;
+                auto rv = that->parse_enum(VTE_TYPE_BLINK, value, v, error);
+                if (rv)
+                        that->blink_mode = VteBlink(v);
+                return rv;
+        }
+
         static gboolean
         parse_cjk_width(char const* option, char const* value, void* data, GError** error)
         {
@@ -341,6 +353,8 @@ public:
                           "Set background image extend", "EXTEND" },
                         { "background-operator", 0, 0, G_OPTION_ARG_CALLBACK, (void*)parse_background_operator,
                           "Set background draw operator", "OPERATOR" },
+                        { "blink", 0, 0, G_OPTION_ARG_CALLBACK, (void*)parse_blink,
+                          "Blink mode (always|focused|unfocused|never)", "MODE" },
                         { "cell-height-scale", 0, 0, G_OPTION_ARG_DOUBLE, &cell_height_scale,
                           "Add extra line spacing", "1.0..2.0" },
                         { "cell-width-scale", 0, 0, G_OPTION_ARG_DOUBLE, &cell_width_scale,
@@ -1837,6 +1851,7 @@ vteapp_window_constructed(GObject *object)
         if (options.word_char_exceptions != nullptr)
                 vte_terminal_set_word_char_exceptions(window->terminal, options.word_char_exceptions);
 
+        vte_terminal_set_allow_blink(window->terminal, options.blink_mode);
         vte_terminal_set_allow_hyperlink(window->terminal, !options.no_hyperlink);
         vte_terminal_set_audible_bell(window->terminal, options.audible_bell);
         vte_terminal_set_bold_is_bright(window->terminal, !options.no_bold_is_bright);
diff --git a/src/vte.cc b/src/vte.cc
index 82661faa..189e3da6 100644
--- a/src/vte.cc
+++ b/src/vte.cc
@@ -433,6 +433,9 @@ 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 */
+        remove_blink_timeout();
+
 	/* replace invalid regions with one covering the whole terminal */
 	reset_update_rects();
 	m_invalidated_all = TRUE;
@@ -4582,6 +4585,16 @@ VteTerminalPrivate::check_cursor_blink()
 		remove_cursor_timeout();
 }
 
+void
+VteTerminalPrivate::remove_blink_timeout()
+{
+	if (m_contents_blink_tag == 0)
+		return;
+
+	g_source_remove(m_contents_blink_tag);
+	m_contents_blink_tag = 0;
+}
+
 void
 VteTerminalPrivate::beep()
 {
@@ -8157,6 +8170,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();
@@ -8198,6 +8212,7 @@ VteTerminalPrivate::VteTerminalPrivate(VteTerminal *t) :
 	set_delete_binding(VTE_ERASE_AUTO);
 	m_meta_sends_escape = TRUE;
 	m_audible_bell = TRUE;
+        m_allow_blink = VTE_BLINK_ALWAYS;
 	m_allow_bold = TRUE;
         m_bold_is_bright = TRUE;
         m_deccolm_mode = FALSE;
@@ -8450,12 +8465,15 @@ VteTerminalPrivate::widget_unrealize()
 		gtk_widget_unmap(m_widget);
 	}
 
-	/* Remove the blink timeout function. */
+	/* Remove the cursor blink timeout function. */
 	remove_cursor_timeout();
 
 	/* Cancel any pending redraws. */
 	remove_update_timeout(this);
 
+        /* Remove the contents blink timeout function. */
+        remove_blink_timeout();
+
 	/* Cancel any pending signals */
 	m_contents_changed_pending = FALSE;
 	m_cursor_moved_pending = FALSE;
@@ -8484,7 +8502,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),
@@ -8886,6 +8904,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 = deco = back;
 	}
@@ -8919,6 +8940,14 @@ VteTerminalPrivate::determine_cursor_colors(VteCell const* cell,
                          fore, back, deco);
 }
 
+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,
@@ -8933,6 +8962,7 @@ VteTerminalPrivate::draw_cells(struct _vte_draw_text_request *items,
                                guint underline,
                                bool strikethrough,
                                bool overline,
+                               bool blink,
                                bool hyperlink,
                                bool hilite,
                                bool boxed,
@@ -8942,6 +8972,7 @@ VteTerminalPrivate::draw_cells(struct _vte_draw_text_request *items,
 	int i, x, y;
 	gint columns = 0;
         vte::color::rgb fg, bg, dc;
+        bool skip_drawing_text = FALSE;
 
 	g_assert(n > 0);
 	_VTE_DEBUG_IF(VTE_DEBUG_CELLS) {
@@ -8952,10 +8983,10 @@ VteTerminalPrivate::draw_cells(struct _vte_draw_text_request *items,
 		}
 		tmp = g_string_free (str, FALSE);
                 g_printerr ("draw_cells('%s', fore=%d, back=%d, deco=%d, bold=%d,"
-                                " ul=%d, strike=%d, ol=%d"
+                                " ul=%d, strike=%d, ol=%d, blink=%d,"
                                 " hyperlink=%d, hilite=%d, boxed=%d)\n",
                                 tmp, fore, back, deco, bold,
-                                underline, strikethrough, overline,
+                                underline, strikethrough, overline, blink,
                                 hyperlink, hilite, boxed);
 		g_free (tmp);
 	}
@@ -8965,6 +8996,29 @@ VteTerminalPrivate::draw_cells(struct _vte_draw_text_request *items,
 	rgb_from_index(back, bg);
         rgb_from_index(deco == VTE_DEFAULT_FG ? fore : deco, dc);
 
+        if (blink &&
+            (m_allow_blink == VTE_BLINK_ALWAYS ||
+             (m_allow_blink == VTE_BLINK_FOCUSED && m_has_focus) ||
+             (m_allow_blink == VTE_BLINK_UNFOCUSED && !m_has_focus))) {
+                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;
@@ -8984,6 +9038,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;
+
         /* Draw whatever SFX are required. Do this before drawing the letters,
          * so that if the descent of a letter crosses an underline of a different color,
          * it's the letter's color that wins. Other kinds of decorations always have the
@@ -9306,6 +9368,7 @@ VteTerminalPrivate::draw_cells_with_attributes(struct _vte_draw_text_request *it
 					cells[j].attr.underline,
 					cells[j].attr.strikethrough,
                                         cells[j].attr.overline,
+                                        cells[j].attr.blink,
                                         m_allow_hyperlink && cells[j].attr.hyperlink_idx != 0,
 					FALSE, FALSE, column_width, height);
 		j += g_unichar_to_utf8(items[i].c, scratch_buf);
@@ -9336,7 +9399,7 @@ VteTerminalPrivate::draw_rows(VteScreen *screen_,
         gboolean bold, nbold, italic, nitalic,
                  hyperlink, nhyperlink, hilite, nhilite,
 		 selected, nselected, strikethrough, nstrikethrough,
-                 overline, noverline, invisible, ninvisible;
+                 overline, noverline, invisible, ninvisible, blink, nblink;
 	guint item_count;
 	const VteCell *cell;
 	VteRowData const* row_data;
@@ -9492,6 +9555,7 @@ VteTerminalPrivate::draw_rows(VteScreen *screen_,
                         hyperlink = (m_allow_hyperlink && cell->attr.hyperlink_idx != 0);
 			bold = cell->attr.bold;
 			italic = cell->attr.italic;
+                        blink = cell->attr.blink;
                         if (cell->attr.hyperlink_idx != 0 && cell->attr.hyperlink_idx == m_hyperlink_hover_idx) {
                                 hilite = true;
                         } else if (m_hyperlink_hover_idx == 0 && m_show_match) {
@@ -9564,6 +9628,10 @@ VteTerminalPrivate::draw_rows(VteScreen *screen_,
                                         if (noverline != overline) {
                                                 break;
                                         }
+                                        nblink = cell->attr.blink;
+                                        if (nblink != blink) {
+                                                break;
+                                        }
                                         nhyperlink = (m_allow_hyperlink && cell->attr.hyperlink_idx != 0);
                                         if (nhyperlink != hyperlink) {
                                                 break;
@@ -9624,8 +9692,8 @@ fg_draw:
 					items,
 					item_count,
                                         fore, back, deco, FALSE, FALSE,
-					bold, italic, underline,
-                                        strikethrough, overline, hyperlink, hilite, FALSE,
+					bold, italic, underline, strikethrough,
+                                        overline, blink, hyperlink, hilite, FALSE,
 					column_width, row_height);
 			item_count = 1;
 			/* We'll need to continue at the first cell which didn't
@@ -9858,6 +9926,7 @@ VteTerminalPrivate::paint_cursor()
                                                         cell->attr.underline,
                                                         cell->attr.strikethrough,
                                                         cell->attr.overline,
+                                                        cell->attr.blink,
                                                         m_allow_hyperlink && cell->attr.hyperlink_idx != 0,
                                                         FALSE,
                                                         FALSE,
@@ -9949,6 +10018,7 @@ VteTerminalPrivate::paint_im_preedit_string()
                                                 0,     /* underline */
                                                 FALSE, /* strikethrough */
                                                 FALSE, /* overline */
+                                                FALSE, /* blink */
                                                 FALSE, /* hyperlink */
                                                 FALSE, /* hilite */
                                                 TRUE,  /* boxed */
@@ -10211,6 +10281,18 @@ VteTerminalPrivate::set_audible_bell(bool setting)
         return true;
 }
 
+bool
+VteTerminalPrivate::set_allow_blink(VteBlink setting)
+{
+        if (setting == m_allow_blink)
+                return false;
+
+        m_allow_blink = setting;
+        invalidate_all();
+
+        return true;
+}
+
 bool
 VteTerminalPrivate::set_allow_bold(bool setting)
 {
diff --git a/src/vte/vteenums.h b/src/vte/vteenums.h
index 0e71715c..48f9008b 100644
--- a/src/vte/vteenums.h
+++ b/src/vte/vteenums.h
@@ -58,6 +58,26 @@ typedef enum {
         VTE_CURSOR_SHAPE_UNDERLINE
 } VteCursorShape;
 
+/**
+ * VteBlink:
+ * @VTE_BLINK_NEVER: Do not blink the contents.
+ * @VTE_BLINK_ALWAYS: Allow blinking contents. This is the default.
+ * @VTE_BLINK_FOCUSED: Allow blinking contents only if the widget is focused.
+ * @VTE_BLINK_UNFOCUSED: Allow blinking contents only if the widget is unfocused.
+ *
+ * An enumerated type which can be used to indicate whether the terminal allows
+ * the contents to be blinked. Note: This is for the actual text contents only,
+ * not for the cursor.
+ *
+ * Since: 0.52
+ */
+typedef enum {
+        VTE_BLINK_NEVER,
+        VTE_BLINK_ALWAYS,
+        VTE_BLINK_FOCUSED,
+        VTE_BLINK_UNFOCUSED
+} VteBlink;
+
 /**
  * VteEraseBinding:
  * @VTE_ERASE_AUTO: For backspace, attempt to determine the right value from the terminal's IO settings.  For delete, use the control sequence.
diff --git a/src/vte/vteterminal.h b/src/vte/vteterminal.h
index 7d85474d..5ceba437 100644
--- a/src/vte/vteterminal.h
+++ b/src/vte/vteterminal.h
@@ -223,6 +223,11 @@ double vte_terminal_get_cell_height_scale(VteTerminal *terminal) _VTE_GNUC_NONNU
 
 /* Set various on-off settings. */
 _VTE_PUBLIC
+void vte_terminal_set_allow_blink(VteTerminal *terminal,
+                                  VteBlink allow_blink) _VTE_GNUC_NONNULL(1);
+_VTE_PUBLIC
+VteBlink vte_terminal_get_allow_blink(VteTerminal *terminal) _VTE_GNUC_NONNULL(1);
+_VTE_PUBLIC
 void vte_terminal_set_audible_bell(VteTerminal *terminal,
                                    gboolean is_audible) _VTE_GNUC_NONNULL(1);
 _VTE_PUBLIC
diff --git a/src/vtedefines.hh b/src/vtedefines.hh
index 89e64161..a962f1ab 100644
--- a/src/vtedefines.hh
+++ b/src/vtedefines.hh
@@ -84,6 +84,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/vtegtk.cc b/src/vtegtk.cc
index fa861304..b2c47e38 100644
--- a/src/vtegtk.cc
+++ b/src/vtegtk.cc
@@ -424,6 +424,9 @@ vte_terminal_get_property (GObject *object,
                 case PROP_VSCROLL_POLICY:
                         g_value_set_enum (value, impl->m_vscroll_policy);
                         break;
+                case PROP_ALLOW_BLINK:
+                        g_value_set_enum (value, vte_terminal_get_allow_blink (terminal));
+                        break;
                 case PROP_ALLOW_BOLD:
                         g_value_set_boolean (value, vte_terminal_get_allow_bold (terminal));
                         break;
@@ -534,6 +537,9 @@ vte_terminal_set_property (GObject *object,
                 case PROP_VSCROLL_POLICY:
                         vte_terminal_set_vscroll_policy(terminal, (GtkScrollablePolicy)g_value_get_enum(value));
                         break;
+                case PROP_ALLOW_BLINK:
+                        vte_terminal_set_allow_blink (terminal, (VteBlink)g_value_get_enum (value));
+                        break;
                 case PROP_ALLOW_BOLD:
                         vte_terminal_set_allow_bold (terminal, g_value_get_boolean (value));
                         break;
@@ -1274,6 +1280,19 @@ vte_terminal_class_init(VteTerminalClass *klass)
                              g_cclosure_marshal_VOID__VOID,
                              G_TYPE_NONE, 0);
 
+        /**
+         * VteTerminal:allow-blink:
+         *
+         * Controls whether or not the terminal will allow blinking text.
+         *
+         * Since: 0.52
+         */
+        pspecs[PROP_ALLOW_BLINK] =
+                g_param_spec_enum ("allow-blink", NULL, NULL,
+                                   VTE_TYPE_BLINK,
+                                   VTE_BLINK_ALWAYS,
+                                   (GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY));
+
         /**
          * VteTerminal:allow-bold:
          *
@@ -2902,6 +2921,42 @@ vte_terminal_set_size(VteTerminal *terminal,
         IMPL(terminal)->set_size(columns, rows);
 }
 
+/**
+ * vte_terminal_get_allow_blink:
+ * @terminal: a #VteTerminal
+ *
+ * Checks whether or not the terminal will allow blinking text.
+ *
+ * Returns: the blinking setting
+ *
+ * Since: 0.52
+ */
+VteBlink
+vte_terminal_get_allow_blink(VteTerminal *terminal)
+{
+        g_return_val_if_fail(VTE_IS_TERMINAL(terminal), VTE_BLINK_ALWAYS);
+        return IMPL(terminal)->m_allow_blink;
+}
+
+/**
+ * vte_terminal_set_allow_blink:
+ * @terminal: a #VteTerminal
+ * @allow_blink: the #VteBlink to use
+ *
+ * Controls whether or not the terminal will allow blinking text.
+ *
+ * Since: 0.52
+ */
+void
+vte_terminal_set_allow_blink(VteTerminal *terminal,
+                             VteBlink allow_blink)
+{
+        g_return_if_fail(VTE_IS_TERMINAL(terminal));
+
+        if (IMPL(terminal)->set_allow_blink(allow_blink))
+                g_object_notify_by_pspec(G_OBJECT(terminal), pspecs[PROP_ALLOW_BLINK]);
+}
+
 /**
  * vte_terminal_get_allow_bold:
  * @terminal: a #VteTerminal
diff --git a/src/vtegtk.hh b/src/vtegtk.hh
index 7adadfe7..0fc4629f 100644
--- a/src/vtegtk.hh
+++ b/src/vtegtk.hh
@@ -63,6 +63,7 @@ extern guint signals[LAST_SIGNAL];
 
 enum {
         PROP_0,
+        PROP_ALLOW_BLINK,
         PROP_ALLOW_BOLD,
         PROP_ALLOW_HYPERLINK,
         PROP_AUDIBLE_BELL,
diff --git a/src/vteinternal.hh b/src/vteinternal.hh
index 2b9e7ba2..d9a6b2ab 100644
--- a/src/vteinternal.hh
+++ b/src/vteinternal.hh
@@ -519,6 +519,10 @@ public:
         gboolean m_cursor_visible;
         gboolean m_has_focus;               /* is the terminal window focused */
 
+        /* Contents blinking */
+        VteBlink m_allow_blink;
+        guint m_contents_blink_tag;
+
         /* DECSCUSR cursor style (shape and blinking possibly overridden
          * via escape sequence) */
         VteCursorStyle m_cursor_style;
@@ -749,6 +753,8 @@ public:
         VteCursorBlinkMode decscusr_cursor_blink();
         VteCursorShape decscusr_cursor_shape();
 
+        void remove_blink_timeout();
+
         /* The allocation of the widget */
         cairo_rectangle_int_t m_allocated_rect;
         /* The usable view area. This is the allocation, minus the padding, but
@@ -844,6 +850,7 @@ public:
                         guint underline,
                         bool strikethrough,
                         bool overline,
+                        bool blink,
                         bool hyperlink,
                         bool hilite,
                         bool boxed,
@@ -1222,6 +1229,7 @@ public:
                          int source);
 
         bool set_audible_bell(bool setting);
+        bool set_allow_blink(VteBlink setting);
         bool set_allow_bold(bool setting);
         bool set_allow_hyperlink(bool setting);
         bool set_backspace_binding(VteEraseBinding binding);
