diff --git a/src/devices/grotty/grotty.1.man b/src/devices/grotty/grotty.1.man
index 44de5df22..339088ce4 100644
--- a/src/devices/grotty/grotty.1.man
+++ b/src/devices/grotty/grotty.1.man
@@ -7,7 +7,7 @@ grotty \- groff output driver for typewriter-like (terminal) devices
 .\" Legal Terms
 .\" ====================================================================
 .\"
-.\" Copyright (C) 1989-2020 Free Software Foundation, Inc.
+.\" Copyright (C) 1989-2021 Free Software Foundation, Inc.
 .\"
 .\" Permission is granted to make and distribute verbatim copies of this
 .\" manual provided the copyright notice and this permission notice are
@@ -105,7 +105,7 @@ reads the standard input stream.
 Output is written to the standard output stream.
 .
 .
-.LP
+.P
 By default,
 .I grotty
 emits SGR escape sequences
@@ -118,8 +118,6 @@ Devices supporting the appropriate sequences can view
 .I roff
 documents using eight different background and foreground colors.
 .
-.
-.LP
 Following ISO\~6429,
 the following colors are defined in
 .IR tty.tmac :
@@ -128,8 +126,10 @@ black, white, red, green, blue, yellow, magenta, and cyan.
 Unrecognized colors are mapped to the default color,
 which is dependent on the settings of the terminal.
 .
+OSC\~8 hyperlinks are produced for these devices.
 .
-.LP
+.
+.P
 In keeping with long-standing practice and the rarity of terminals,
 hardware or emulated,
 that support oblique or italic fonts,
@@ -251,13 +251,41 @@ There is therefore no need to filter its output through
 .\" ====================================================================
 .
 .I grotty
-understands a single device control function produced using the
+understands the following device control functions produced using the
 .I roff
 .B \[rs]X
 escape sequence in a document.
 .
 .
 .TP
+.BR "\[rs]X\[aq]tty: link " [\c
+.IR uri \~[ key\c
+.BI = value\c
+] \|.\|.\|.\|]\c
+.B \[aq]
+.
+Embed a hyperlink using the OSC 8 terminal escape sequence.
+.
+Specifying
+.I uri
+starts hyperlinked text,
+and omitting it ends the hyperlink.
+.
+When
+.I uri
+is is present,
+any number of additional key/value pairs can be specified;
+their interpretation is the responsibility of the terminal emulator.
+.
+Spaces or tabs cannot appear literally in
+.IR uri ,
+.IR key ,
+or
+.IR value ;
+they must be represented in an alternate form.
+.
+.
+.TP
 .BR "\[rs]X\[aq]tty: sgr " [\c
 .IR n ]\c
 .B \[aq]
@@ -269,6 +297,12 @@ is non-zero or missing, enable SGR sequences
 otherwise,
 use the legacy output format.
 .
+This device control command,
+like the
+.B \-c
+option,
+also prevents the emission of terminal OSC 8 hyperlink escape sequences.
+.
 .
 .\" ====================================================================
 .SS "Device description files"
@@ -354,6 +388,8 @@ Use
 legacy output format
 (see subsection \[lq]Legacy output format\[rq] above).
 .
+OSC\~8 hyperlink terminal escape sequences are not emitted.
+.
 .
 .TP
 .B \-d
@@ -705,7 +741,15 @@ ECMA\-ST/\:Ecma\-048\:.pdf
 .UE .
 .
 .
-.LP
+.P
+.UR https://\:gist\:.github\:.com/\:egmontkob/\:\
+eb114294efbcd5adb1944c9f3cb5feda
+\[lq]Hyperlinks in Terminal Emulators\[rq]
+.UE ,
+Egmont Koblinger.
+.
+.
+.P
 .IR groff (@MAN1EXT@),
 .IR \%@g@troff (@MAN1EXT@),
 .IR groff_out (@MAN5EXT@),
diff --git a/src/devices/grotty/tty.cpp b/src/devices/grotty/tty.cpp
index 3efd852a5..e8e12a3c1 100644
--- a/src/devices/grotty/tty.cpp
+++ b/src/devices/grotty/tty.cpp
@@ -1,4 +1,3 @@
-// -*- C++ -*-
 /* Copyright (C) 1989-2020 Free Software Foundation, Inc.
      Written by James Clark (jjc@jclark.com)
 
@@ -80,8 +79,12 @@ static unsigned char bold_underline_mode;
 
 #ifndef IS_EBCDIC_HOST
 #define CSI "\033["
+#define OSC8 "\033]8"
+#define ST "\033\\"
 #else
 #define CSI "\047["
+#define OSC8 "\047]8"
+#define ST "\047\\"
 #endif
 
 // SGR handling (ISO 6429)
@@ -183,12 +186,14 @@ class tty_printer : public printer {
   schar color_to_idx(color *);
   void add_char(output_character, int, int, int, color *, color *,
 		unsigned char);
+  void simple_add_char(const output_character, const environment *);
   char *make_rgb_string(unsigned int, unsigned int, unsigned int);
   int tty_color(unsigned int, unsigned int, unsigned int, schar *,
 		schar = DEFAULT_COLOR_IDX);
   void line(int, int, int, int, color *, color *);
   void draw_line(int *, int, const environment *);
   void draw_polygon(int *, int, const environment *);
+  void special_link(char *, const environment *);
 public:
   tty_printer();
   ~tty_printer();
@@ -404,6 +409,12 @@ void tty_printer::add_char(output_character c, int w,
   *pp = g;
 }
 
+void tty_printer::simple_add_char(const output_character c,
+				  const environment *env)
+{
+  add_char(c, 0, env->hpos, env->vpos, env->col, env->fill, 0);
+}
+
 void tty_printer::special(char *arg, const environment *env, char type)
 {
   if (type == 'u') {
@@ -443,6 +454,65 @@ void tty_printer::special(char *arg, const environment *env, char type)
       old_drawing_scheme = 0;
     update_options();
   }
+  else if (strncmp(command, "link", p - command) == 0)
+    special_link(p, env);
+}
+
+// Produce an OSC 8 hyperlink.  Given ditroff input of the form:
+//   x X tty: link [URI[ KEY=VALUE] ...]
+// produce "OSC 8 [;KEY=VALUE:]...;[URI]; ST ".  KEY/VALUE pairs can be
+// repeated arbitrarily and are separated by colons.  Omission of the
+// URI ends the hyperlink that was begun by specifying it.  See
+// <https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda>.
+void tty_printer::special_link(char *arg, const environment *env)
+{
+  if (old_drawing_scheme)
+    return;
+  for (const char *s = OSC8; *s != '\0'; s++)
+    simple_add_char(*s, env);
+  simple_add_char(';', env);
+  char c = *arg;
+  if ('\0' == c || '\n' == c) {
+    simple_add_char(';', env);
+  }
+  else {
+    assert(c == ' ' || c == '\t');
+    do
+      c = *arg++;
+    while (c == ' ' || c == '\t');
+    arg--;
+    // The first argument is the URI.
+    char *uri = arg;
+    size_t uri_len = 0;
+    uri = arg;
+    while (c != '\0' && c != ' ' && c != '\t') {
+      c = *arg++;
+      uri_len++;
+    }
+    // The remaining arguments are "key=value" pairs.
+    char *pair = 0;
+    bool done = false;
+    do {
+      if (pair != 0)
+	simple_add_char(':', env);
+      pair = arg;
+      bool in_pair = true;
+      do {
+	c = *arg++;
+	if ('\0' == c)
+	  done = true;
+	else if (' ' == c || '\t' == c)
+	  in_pair = false;
+	else
+	  simple_add_char(c, env);
+      } while (!done && in_pair);
+    } while (!done);
+    simple_add_char(';', env);
+    for (size_t i = uri_len; i > 0; i--)
+      simple_add_char(*uri++, env);
+  }
+  for (const char *s = ST; *s != '\0'; s++)
+    simple_add_char(*s, env);
 }
 
 void tty_printer::change_color(const environment * const env)
@@ -926,3 +996,9 @@ static void usage(FILE *stream)
   fprintf(stream, "usage: %s [-bBcdfhioruUv] [-F dir] [files ...]\n",
 	  program_name);
 }
+
+// Local Variables:
+// fill-column: 72
+// mode: C++
+// End:
+// vim: set cindent noexpandtab shiftwidth=2 textwidth=72:
diff --git a/tmac/an-ext.tmac b/tmac/an-ext.tmac
index 956d28804..2790df49d 100644
--- a/tmac/an-ext.tmac
+++ b/tmac/an-ext.tmac
@@ -26,11 +26,15 @@
 .if \n(mX>1 \
 .  nx
 .
-.\" Check whether we are using grohtml.
+.\" Check whether we are using grohtml or grotty.
 .nr mH 0
-.if \n(.g \
+.nr mY 0
+.if \n(.g \{\
 .  if '\*(.T'html' \
 .    nr mH 1
+.  if '\*(.T'utf8' \
+.    nr mY 1
+.\}
 .
 .
 .\" Map mono-width fonts to standard fonts for groff's TTY device.
@@ -88,6 +92,8 @@
 .\" Start URL.
 .de UR
 .  ds m1 \\$1\"
+.  if \\n(mY \
+\X^tty: link \\$1^\c
 .  if \\n(mH \{\
 .    \" Start diversion in a new environment.
 .    do ev link-text-env
@@ -98,6 +104,8 @@
 .
 .\" End URL.
 .de UE
+.  if \\n(mY \
+\X^tty: link^\c
 .  ie \\n(mH \{\
 .    br
 .    di
@@ -128,6 +136,8 @@
 .
 .\" Start email address.
 .de MT
+.  if \\n(mY \
+\X^tty: link \\$1^\c
 .  ds m1 \\$1\"
 .  if \\n(mH \{\
 .    \" Start diversion in a new environment.
@@ -139,6 +149,8 @@
 .
 .\" End email address.
 .de ME
+.  if \\n(mY \
+\X^tty: link^\c
 .  ie \\n(mH \{\
 .    br
 .    di
