diff --git a/.eslintrc.json b/.eslintrc.json
index f234e93cc5..20b111cbce 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -13,6 +13,7 @@
       "test/benchmark/tsconfig.json",
       "addons/xterm-addon-attach/src/tsconfig.json",
       "addons/xterm-addon-fit/src/tsconfig.json",
+      "addons/xterm-addon-hyperlinks/src/tsconfig.json",
       "addons/xterm-addon-search/src/tsconfig.json",
       "addons/xterm-addon-unicode11/src/tsconfig.json",
       "addons/xterm-addon-web-links/src/tsconfig.json",
diff --git a/addons/xterm-addon-hyperlinks/.gitignore b/addons/xterm-addon-hyperlinks/.gitignore
new file mode 100644
index 0000000000..3063f07d55
--- /dev/null
+++ b/addons/xterm-addon-hyperlinks/.gitignore
@@ -0,0 +1,2 @@
+lib
+node_modules
diff --git a/addons/xterm-addon-hyperlinks/.npmignore b/addons/xterm-addon-hyperlinks/.npmignore
new file mode 100644
index 0000000000..1c79444565
--- /dev/null
+++ b/addons/xterm-addon-hyperlinks/.npmignore
@@ -0,0 +1,5 @@
+**/*.api.js
+**/*.api.ts
+tsconfig.json
+.yarnrc
+webpack.config.js
diff --git a/addons/xterm-addon-hyperlinks/LICENSE b/addons/xterm-addon-hyperlinks/LICENSE
new file mode 100644
index 0000000000..c1735c41a7
--- /dev/null
+++ b/addons/xterm-addon-hyperlinks/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2020, The xterm.js authors (https://github.com/xtermjs/xterm.js)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/addons/xterm-addon-hyperlinks/README.md b/addons/xterm-addon-hyperlinks/README.md
new file mode 100644
index 0000000000..a6db2910e8
--- /dev/null
+++ b/addons/xterm-addon-hyperlinks/README.md
@@ -0,0 +1,18 @@
+## xterm-addon-hyperlinks
+
+An addon providing support for the hyperlinks terminal sequence with `OSC 8`.
+
+### Install
+
+```bash
+npm install --save xterm-addon-hyperlinks
+```
+
+### Usage
+
+```ts
+import { Terminal } from 'xterm';
+import { HyperlinksAddon } from 'xterm-addon-hyperlinks';
+
+// TODO...
+```
diff --git a/addons/xterm-addon-hyperlinks/package.json b/addons/xterm-addon-hyperlinks/package.json
new file mode 100644
index 0000000000..2caec78e99
--- /dev/null
+++ b/addons/xterm-addon-hyperlinks/package.json
@@ -0,0 +1,21 @@
+{
+  "name": "xterm-addon-hyperlinks",
+  "version": "0.1.0",
+  "author": {
+    "name": "The xterm.js authors",
+    "url": "https://xtermjs.org/"
+  },
+  "main": "lib/xterm-addon-hyperlinks.js",
+  "types": "typings/xterm-addon-hyperlinks.d.ts",
+  "repository": "https://github.com/xtermjs/xterm.js",
+  "license": "MIT",
+  "scripts": {
+    "build": "../../node_modules/.bin/tsc -p src",
+    "prepackage": "npm run build",
+    "package": "../../node_modules/.bin/webpack",
+    "prepublishOnly": "npm run package"
+  },
+  "peerDependencies": {
+    "xterm": "^4.0.0"
+  }
+}
diff --git a/addons/xterm-addon-hyperlinks/src/HyperlinksAddon.api.ts b/addons/xterm-addon-hyperlinks/src/HyperlinksAddon.api.ts
new file mode 100644
index 0000000000..7282546e7c
--- /dev/null
+++ b/addons/xterm-addon-hyperlinks/src/HyperlinksAddon.api.ts
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2019 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { assert } from 'chai';
+import { openTerminal, getBrowserType } from '../../../out-test/api/TestUtils';
+import { Browser, Page } from 'playwright-core';
+
+const APP = 'http://127.0.0.1:3000/test';
+
+let browser: Browser;
+let page: Page;
+const width = 800;
+const height = 600;
+
+describe('HyperlinksAddon', () => {
+  before(async function(): Promise<any> {
+    const browserType = getBrowserType();
+    browser = await browserType.launch({ dumpio: true,
+      headless: process.argv.indexOf('--headless') !== -1
+    });
+    page = await (await browser.newContext()).newPage();
+    await page.setViewportSize({ width, height });
+  });
+
+  after(async () => {
+    await browser.close();
+  });
+
+  beforeEach(async function(): Promise<any> {
+    await page.goto(APP);
+    await openTerminal(page);
+  });
+
+  // TODO: write some tests
+});
diff --git a/addons/xterm-addon-hyperlinks/src/HyperlinksAddon.ts b/addons/xterm-addon-hyperlinks/src/HyperlinksAddon.ts
new file mode 100644
index 0000000000..1e06f188f3
--- /dev/null
+++ b/addons/xterm-addon-hyperlinks/src/HyperlinksAddon.ts
@@ -0,0 +1,302 @@
+/**
+ * Copyright (c) 2019 The xterm.js authors. All rights reserved.
+ * @license MIT
+ *
+ * UnicodeVersionProvider for V11.
+ */
+
+import { Terminal, ITerminalAddon, IDisposable, IBufferRange, ILinkProvider, IBufferCellPosition, ILink } from 'xterm';
+
+/**
+ * TODO:
+ * Need the following changes in xterm.js:
+ * - allow LinkProvider callback to contain multiple ranges (currently sloppy hacked into Linkifier2)
+ * - make extended attributes contributable by outer code
+ * - extend public API by extended attributes
+ */
+
+/**
+ * Configurable scheme link handler.
+ */
+interface ISchemeHandler {
+  matcher: RegExp;
+  opener: (event: MouseEvent, text: string) => void;
+}
+
+interface IUrlWithHandler {
+  url: string;
+  schemeHandler: ISchemeHandler;
+}
+
+
+/**
+ * Default link handler. Opens the link in a new browser tab.
+ * Used by the default scheme handlers for http, https and ftp.
+ */
+function handleLink(event: MouseEvent, uri: string): void {
+  const newWindow = window.open();
+  if (newWindow) {
+    newWindow.opener = null;
+    newWindow.location.href = uri;
+  } else {
+    console.warn('Opening link blocked as opener could not be cleared');
+  }
+}
+
+
+/**
+ * Some default scheme handlers. Handled in-browser.
+ */
+export const DEFAULT_SCHEMES = {
+  HTTP: {matcher: new RegExp('^http://.*'), opener: handleLink},
+  HTTPS: {matcher: new RegExp('^http://.*'), opener: handleLink},
+  FTP: {matcher: new RegExp('^ftp://.*'), opener: handleLink}
+};
+
+
+// limit stored URLs to avoid OOM
+// cached by the addon (FIFO): limit <= n <= limit * 2
+const CACHE_LIMIT = 500;
+// values taken from spec as used by VTE
+const MAX_URL_LENGTH = 2083;
+const MAX_ID_LENGTH = 250;
+
+
+export class HyperlinksAddon implements ITerminalAddon {
+  private _oscHandler: IDisposable | undefined;
+  private _linkProvider: IDisposable | undefined;
+  private _internalId = 1;
+  private _lowestId = 1;
+  private _idMap: Map<string, number> = new Map();
+  private _urlMap: Map<number, IUrlWithHandler> = new Map();
+  private _schemes: ISchemeHandler[] = [];
+
+  constructor(
+    public cacheLimit: number = CACHE_LIMIT,
+    public maxUrlLength: number = MAX_URL_LENGTH,
+    public maxIdLength: number = MAX_ID_LENGTH
+  ) {}
+
+  /**
+   * Parse params part of the data.
+   * Converts into a key-value mapping object if:
+   * - fully empty (returns empty object)
+   * - all keys and values are set
+   * - key-value pairs are properly separated by ':'
+   * - keys and values are separated by '='
+   * - a key-value pair can be empty
+   * Any other case will drop to `false` as return value.
+   */
+  private _parseParams(paramString: string): {[key: string]: string} | void {
+    const result: {[key: string]: string} = {};
+    const params = paramString.split(':').filter(Boolean).map(el => el.split('='));
+    for (const p of params) {
+      if (p.length !== 2) {
+        return;
+      }
+      const [key, value] = p;
+      if (!key || !value || value.length > this.maxIdLength) {
+        return;
+      }
+      result[key] = value;
+    }
+    return result;
+  }
+
+  /**
+   * Method to filter allowed URL schemes.
+   * Returns the url with the matching handler, or nothing.
+   */
+  private _filterUrl(urlString: string): IUrlWithHandler | void {
+    if (!urlString || urlString.length > this.maxUrlLength) {
+      return;
+    }
+    for (const schemeHandler of this._schemes) {
+      const m = urlString.match(schemeHandler.matcher);
+      if (m) {
+        return {url: m[0], schemeHandler};
+      }
+    }
+    return;
+  }
+
+  /**
+   * Update the terminal cell attributes.
+   * `hoverId` is used to identify cells of the same link.
+   * A `hoverId` of 0 disables any link handling of a cell (default).
+   */
+  private _updateAttrs(terminal: Terminal, hoverId: number = 0): void {
+    // hack: remove url notion from extended attrs by private access
+    // TODO: API to contribute to extended attributes
+    const attr = (terminal as any)._core._inputHandler._curAttrData;
+    attr.extended = attr.extended.clone();
+    attr.extended.urlId = hoverId;
+    attr.updateExtended();
+  }
+
+  private _limitCache(): void {
+    if (this._internalId - this._lowestId > this.cacheLimit * 2) {
+      this._lowestId = this._internalId - this.cacheLimit;
+      [...this._urlMap.keys()]
+        .filter(key => key < this._lowestId)
+        .forEach(key => this._urlMap.delete(key));
+      [...this._idMap.entries()]
+        .filter(([unused, value]) => value < this._lowestId)
+        .forEach(([key, unused]) => this._idMap.delete(key));
+    }
+  }
+
+  public activate(terminal: Terminal): void {
+    // register the OSC 8 sequence handler
+    this._oscHandler = terminal.parser.registerOscHandler(8, data => {
+      // always reset URL notion / link handling in buffer
+      // This is a safety measure, any OSC 8 invocations (even malformed)
+      // are treated as an attempt to finalize a previous url start.
+      this._updateAttrs(terminal);
+
+      // malformed, exit early
+      if (data.indexOf(';') === -1) {
+        return true;
+      }
+
+      // extract needed bits of the sequence: params (might hold an id), url
+      const params = this._parseParams(data.slice(0, data.indexOf(';')));
+      const urlData = this._filterUrl(data.slice(data.indexOf(';') + 1));
+
+      // OSC 8 ; ; ST - official URL anchor end marker
+      if (!urlData) {
+        return true;
+      }
+
+      // OSC 8 ; [id=value] ; url ST - official URL anchor starter
+
+      let hoverId;
+      if (params && params.id) {
+        // an id was given, thus try to match with earlier sequences
+        // we only consider full equality (id && url) as match,
+        // as the id might get reused by a later program with different url
+        const oldInternal = this._idMap.get(params.id);
+        if (oldInternal && this._urlMap.get(oldInternal)?.url === urlData.url) {
+          hoverId = oldInternal;
+        } else {
+          hoverId = this._internalId++;
+          this._idMap.set(params.id, hoverId);
+          this._urlMap.set(hoverId, urlData);
+        }
+      } else {
+        hoverId = this._internalId++;
+        this._urlMap.set(hoverId, urlData);
+      }
+
+      // cleanup maps
+      this._limitCache();
+
+      // update extended cell attributes with hoverId
+      this._updateAttrs(terminal, hoverId);
+      return true;
+    });
+
+    // register a linkprovider to handle the extended attribute set by the sequence handler
+    this._linkProvider = terminal.registerLinkProvider(new HyperlinkProvider(terminal, this._urlMap));
+  }
+
+  public dispose(): void {
+    this._oscHandler?.dispose();
+    this._linkProvider?.dispose();
+    this._idMap.clear();
+    this._urlMap.clear();
+    this._schemes.length = 0;
+  }
+
+  /**
+   * Register a scheme handler for the hyperlinks addon.
+   */
+  public registerSchemeHandler(exe: ISchemeHandler): IDisposable {
+    const schemes = this._schemes;
+    if (schemes.indexOf(exe) === -1) {
+      schemes.push(exe);
+    }
+    return {
+      dispose: () => {
+        if (schemes.indexOf(exe) !== -1) {
+          schemes.splice(schemes.indexOf(exe), 1);
+        }
+      }
+    };
+  }
+}
+
+
+class HyperlinkProvider implements ILinkProvider {
+  constructor(
+    private readonly _terminal: Terminal,
+    private readonly _urlMap: Map<number, IUrlWithHandler>
+  ) {}
+
+  public provideLink(position: IBufferCellPosition, callback: (link: ILink | undefined) => void): void {
+    // fix position to 0-based right exclusive
+    const pos = {x: position.x - 1, y: position.y - 1};
+
+    // test whether the pointer is over a cell with an urlId
+    // also test that we actually have an url stored for the id
+    // TODO: need API extension
+    const urlId = (this._terminal.buffer.active.getLine(pos.y)?.getCell(pos.x) as any).extended.urlId;
+    if (!urlId || !this._urlMap.get(urlId)) {
+      callback(undefined);
+      return;
+    }
+
+    // walk all viewport cells and collect cells with the same urlId in buffer ranges
+    const yOffset = this._terminal.buffer.active.viewportY;
+    const ranges: IBufferRange[] = [];
+    let r: IBufferRange | null = null;
+    for (let y = yOffset; y < this._terminal.rows + yOffset; ++y) {
+      const line = this._terminal.buffer.active.getLine(y);
+      if (!line) {
+        break;
+      }
+      for (let x = 0; x < this._terminal.rows; ++x) {
+        const cell = line.getCell(x);
+        if (!cell) {
+          break;
+        }
+        if ((cell as any).extended.urlId === urlId) {
+          if (!r) {
+            r = {start: {x, y}, end: {x: x + 1, y}};
+          } else {
+            r.end.x = x + 1;
+            r.end.y = y;
+          }
+        } else {
+          if (r) {
+            r.end.x = x;
+            r.end.y = y;
+            ranges.push(r);
+            r = null;
+          }
+        }
+      }
+    }
+    if (r) {
+      ranges.push(r);
+    }
+
+    // fix ranges to 1-based, right inclusive
+    for (const r of ranges) {
+      r.start.x++;
+      r.start.y++;
+      r.end.y++;
+    }
+
+    // TODO: make this better customizable from outside
+    callback({
+      ranges,
+      text: this._urlMap.get(urlId)!.url,
+      decorations: {pointerCursor: true, underline: true},
+      activate: this._urlMap.get(urlId)!.schemeHandler.opener,
+      hover: (event: MouseEvent, text: string) => {
+        console.log('tooltip to show:', text);
+      }
+    });
+  }
+}
diff --git a/addons/xterm-addon-hyperlinks/src/tsconfig.json b/addons/xterm-addon-hyperlinks/src/tsconfig.json
new file mode 100644
index 0000000000..f9d0ab5fab
--- /dev/null
+++ b/addons/xterm-addon-hyperlinks/src/tsconfig.json
@@ -0,0 +1,34 @@
+{
+  "compilerOptions": {
+    "module": "commonjs",
+    "target": "es2015",
+    "lib": [
+      "dom",
+      "es2015"
+    ],
+    "rootDir": ".",
+    "outDir": "../out",
+    "sourceMap": true,
+    "removeComments": true,
+    "strict": true,
+    "baseUrl": ".",
+    "paths": {
+      "common/*": [
+        "../../../src/common/*"
+      ]
+    },
+    "types": [
+      "../../../node_modules/@types/mocha",
+      "../../../out-test/api/TestUtils"
+    ]
+  },
+  "include": [
+    "./**/*",
+    "../../../typings/xterm.d.ts"
+  ],
+  "references": [
+    {
+      "path": "../../../src/common"
+    }
+  ]
+}
diff --git a/addons/xterm-addon-hyperlinks/typings/xterm-addon-hyperlinks.d.ts b/addons/xterm-addon-hyperlinks/typings/xterm-addon-hyperlinks.d.ts
new file mode 100644
index 0000000000..07acd01500
--- /dev/null
+++ b/addons/xterm-addon-hyperlinks/typings/xterm-addon-hyperlinks.d.ts
@@ -0,0 +1,20 @@
+/**
+ * Copyright (c) 2017 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { Terminal, ITerminalAddon, IDisposable } from 'xterm';
+
+export interface ISchemeHandler {
+  matcher: RegExp;
+  opener: (event: MouseEvent, text: string) => void
+}
+
+declare module 'xterm-addon-hyperlinks' {
+  export class HyperlinksAddon implements ITerminalAddon {
+    constructor();
+    public activate(terminal: Terminal): void;
+    public dispose(): void;
+    public registerSchemeHandler(exe: ISchemeHandler): IDisposable;
+  }
+}
diff --git a/addons/xterm-addon-hyperlinks/webpack.config.js b/addons/xterm-addon-hyperlinks/webpack.config.js
new file mode 100644
index 0000000000..47901f6a67
--- /dev/null
+++ b/addons/xterm-addon-hyperlinks/webpack.config.js
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2019 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+const path = require('path');
+
+const addonName = 'HyperlinksAddon';
+const mainFile = 'xterm-addon-hyperlinks.js';
+
+module.exports = {
+  entry: `./out/${addonName}.js`,
+  devtool: 'source-map',
+  module: {
+    rules: [
+      {
+        test: /\.js$/,
+        use: ["source-map-loader"],
+        enforce: "pre",
+        exclude: /node_modules/
+      }
+    ]
+  },
+  resolve: {
+    modules: ['./node_modules'],
+    extensions: [ '.js' ],
+    alias: {
+      common: path.resolve('../../out/common')
+    }
+  },
+  output: {
+    filename: mainFile,
+    path: path.resolve('./lib'),
+    library: addonName,
+    libraryTarget: 'umd'
+  },
+  mode: 'production'
+};
diff --git a/addons/xterm-addon-web-links/src/WebLinkProvider.ts b/addons/xterm-addon-web-links/src/WebLinkProvider.ts
index 563108d29e..1d1fa621b0 100644
--- a/addons/xterm-addon-web-links/src/WebLinkProvider.ts
+++ b/addons/xterm-addon-web-links/src/WebLinkProvider.ts
@@ -68,7 +68,7 @@ export class LinkComputer {
         }
       };
 
-      return { range, text, activate: handler };
+      return { ranges: [range], text, activate: handler };
     }
   }
 
diff --git a/demo/client.ts b/demo/client.ts
index 3cb1b3ec73..da400a7f8c 100644
--- a/demo/client.ts
+++ b/demo/client.ts
@@ -11,6 +11,7 @@
 import { Terminal } from '../out/browser/public/Terminal';
 import { AttachAddon } from '../addons/xterm-addon-attach/out/AttachAddon';
 import { FitAddon } from '../addons/xterm-addon-fit/out/FitAddon';
+import { HyperlinksAddon, DEFAULT_SCHEMES } from '../addons/xterm-addon-hyperlinks/out/HyperlinksAddon';
 import { SearchAddon, ISearchOptions } from '../addons/xterm-addon-search/out/SearchAddon';
 import { SerializeAddon } from '../addons/xterm-addon-serialize/out/SerializeAddon';
 import { WebLinksAddon } from '../addons/xterm-addon-web-links/out/WebLinksAddon';
@@ -36,6 +37,7 @@ export interface IWindowWithTerminal extends Window {
   Terminal?: typeof TerminalType;
   AttachAddon?: typeof AttachAddon;
   FitAddon?: typeof FitAddon;
+  HyperlinksAddon?: typeof HyperlinksAddon;
   SearchAddon?: typeof SearchAddon;
   SerializeAddon?: typeof SerializeAddon;
   WebLinksAddon?: typeof WebLinksAddon;
@@ -50,7 +52,7 @@ let socketURL;
 let socket;
 let pid;
 
-type AddonType = 'attach' | 'fit' | 'search' | 'serialize' | 'unicode11' | 'web-links' | 'webgl';
+type AddonType = 'attach' | 'fit' | 'hyperlinks' | 'search' | 'serialize' | 'unicode11' | 'web-links' | 'webgl';
 
 interface IDemoAddon<T extends AddonType> {
   name: T;
@@ -58,6 +60,7 @@ interface IDemoAddon<T extends AddonType> {
   ctor:
     T extends 'attach' ? typeof AttachAddon :
     T extends 'fit' ? typeof FitAddon :
+    T extends 'hyperlinks' ? typeof HyperlinksAddon:
     T extends 'search' ? typeof SearchAddon :
     T extends 'serialize' ? typeof SerializeAddon :
     T extends 'web-links' ? typeof WebLinksAddon :
@@ -66,6 +69,7 @@ interface IDemoAddon<T extends AddonType> {
   instance?:
     T extends 'attach' ? AttachAddon :
     T extends 'fit' ? FitAddon :
+    T extends 'hyperlinks' ? typeof HyperlinksAddon:
     T extends 'search' ? SearchAddon :
     T extends 'serialize' ? SerializeAddon :
     T extends 'web-links' ? WebLinksAddon :
@@ -77,6 +81,7 @@ interface IDemoAddon<T extends AddonType> {
 const addons: { [T in AddonType]: IDemoAddon<T>} = {
   attach: { name: 'attach', ctor: AttachAddon, canChange: false },
   fit: { name: 'fit', ctor: FitAddon, canChange: false },
+  hyperlinks: { name: 'hyperlinks', ctor: HyperlinksAddon, canChange: true },
   search: { name: 'search', ctor: SearchAddon, canChange: true },
   serialize: { name: 'serialize', ctor: SerializeAddon, canChange: true },
   'web-links': { name: 'web-links', ctor: WebLinksAddon, canChange: true },
@@ -114,6 +119,7 @@ const disposeRecreateButtonHandler = () => {
     socket = null;
     addons.attach.instance = undefined;
     addons.fit.instance = undefined;
+    addons.hyperlinks.instance = undefined;
     addons.search.instance = undefined;
     addons.serialize.instance = undefined;
     addons.unicode11.instance = undefined;
@@ -130,6 +136,7 @@ if (document.location.pathname === '/test') {
   window.Terminal = Terminal;
   window.AttachAddon = AttachAddon;
   window.FitAddon = FitAddon;
+  window.HyperlinksAddon = HyperlinksAddon;
   window.SearchAddon = SearchAddon;
   window.SerializeAddon = SerializeAddon;
   window.Unicode11Addon = Unicode11Addon;
@@ -157,10 +164,15 @@ function createTerminal(): void {
   addons.search.instance = new SearchAddon();
   addons.serialize.instance = new SerializeAddon();
   addons.fit.instance = new FitAddon();
+  addons.hyperlinks.instance = new HyperlinksAddon();
+  addons.hyperlinks.instance.registerSchemeHandler(DEFAULT_SCHEMES.HTTP);
+  addons.hyperlinks.instance.registerSchemeHandler(DEFAULT_SCHEMES.HTTPS);
+  addons.hyperlinks.instance.registerSchemeHandler(DEFAULT_SCHEMES.FTP);
   addons.unicode11.instance = new Unicode11Addon();
   // TODO: Remove arguments when link provider API is the default
   addons['web-links'].instance = new WebLinksAddon(undefined, undefined, true);
   typedTerm.loadAddon(addons.fit.instance);
+  typedTerm.loadAddon(addons.hyperlinks.instance);
   typedTerm.loadAddon(addons.search.instance);
   typedTerm.loadAddon(addons.serialize.instance);
   typedTerm.loadAddon(addons.unicode11.instance);
@@ -367,6 +379,11 @@ function initAddons(term: TerminalType): void {
       if (checkbox.checked) {
         addon.instance = new addon.ctor();
         term.loadAddon(addon.instance);
+        if (name === 'hyperlinks') {
+          addon.instance.registerSchemeHandler(DEFAULT_SCHEMES.HTTP);
+          addon.instance.registerSchemeHandler(DEFAULT_SCHEMES.HTTPS);
+          addon.instance.registerSchemeHandler(DEFAULT_SCHEMES.FTP);
+        }
         if (name === 'webgl') {
           setTimeout(() => {
             document.body.appendChild((addon.instance as WebglAddon).textureAtlas);
diff --git a/src/browser/Linkifier2.test.ts b/src/browser/Linkifier2.test.ts
index fb71b6aaa4..f309a3b301 100644
--- a/src/browser/Linkifier2.test.ts
+++ b/src/browser/Linkifier2.test.ts
@@ -42,7 +42,7 @@ describe('Linkifier2', () => {
 
   const link: ILink = {
     text: 'foo',
-    range: {
+    ranges: [{
       start: {
         x: 5,
         y: 1
@@ -51,16 +51,16 @@ describe('Linkifier2', () => {
         x: 7,
         y: 1
       }
-    },
+    }],
     activate: () => { }
   };
 
   it('onShowLinkUnderline event range is correct', done => {
     linkifier.onShowLinkUnderline(e => {
-      assert.equal(link.range.start.x - 1, e.x1);
-      assert.equal(link.range.start.y - 1, e.y1);
-      assert.equal(link.range.end.x, e.x2);
-      assert.equal(link.range.end.y - 1, e.y2);
+      assert.equal(link.ranges[0].start.x - 1, e.x1);
+      assert.equal(link.ranges[0].start.y - 1, e.y1);
+      assert.equal(link.ranges[0].end.x, e.x2);
+      assert.equal(link.ranges[0].end.y - 1, e.y2);
 
       done();
     });
@@ -70,10 +70,10 @@ describe('Linkifier2', () => {
 
   it('onHideLinkUnderline event range is correct', done => {
     linkifier.onHideLinkUnderline(e => {
-      assert.equal(link.range.start.x - 1, e.x1);
-      assert.equal(link.range.start.y - 1, e.y1);
-      assert.equal(link.range.end.x, e.x2);
-      assert.equal(link.range.end.y - 1, e.y2);
+      assert.equal(link.ranges[0].start.x - 1, e.x1);
+      assert.equal(link.ranges[0].start.y - 1, e.y1);
+      assert.equal(link.ranges[0].end.x, e.x2);
+      assert.equal(link.ranges[0].end.y - 1, e.y2);
 
       done();
     });
diff --git a/src/browser/Linkifier2.ts b/src/browser/Linkifier2.ts
index 39f750cc02..c317ebe22a 100644
--- a/src/browser/Linkifier2.ts
+++ b/src/browser/Linkifier2.ts
@@ -178,7 +178,8 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
     }
 
     // If we have a start and end row, check that the link is within it
-    if (!startRow || !endRow || (this._currentLink.range.start.y >= startRow && this._currentLink.range.end.y <= endRow)) {
+    // FIXME: needs proper handling of multiple ranges
+    if (!startRow || !endRow || (this._currentLink.ranges[0].start.y >= startRow && this._currentLink.ranges[0].end.y <= endRow)) {
       this._linkLeave(this._element, this._currentLink, this._lastMouseEvent);
       this._currentLink = undefined;
       this._currentLinkState = undefined;
@@ -266,11 +267,13 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
   }
 
   private _fireUnderlineEvent(link: ILink, showEvent: boolean): void {
-    const range = link.range;
-    const scrollOffset = this._bufferService.buffer.ydisp;
-    const event = this._createLinkUnderlineEvent(range.start.x - 1, range.start.y - scrollOffset - 1, range.end.x, range.end.y - scrollOffset - 1, undefined);
-    const emitter = showEvent ? this._onShowLinkUnderline : this._onHideLinkUnderline;
-    emitter.fire(event);
+    // FIXME: dirty hack to get everything underlined, needs proper handling
+    for (const range of link.ranges) {
+      const scrollOffset = this._bufferService.buffer.ydisp;
+      const event = this._createLinkUnderlineEvent(range.start.x - 1, range.start.y - scrollOffset - 1, range.end.x, range.end.y - scrollOffset - 1, undefined);
+      const emitter = showEvent ? this._onShowLinkUnderline : this._onHideLinkUnderline;
+      emitter.fire(event);
+    }
   }
 
   protected _linkLeave(element: HTMLElement, link: ILink, event: MouseEvent): void {
@@ -294,19 +297,28 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
    * @param link
    * @param position
    */
-  private _linkAtPosition(link: ILink, position: IBufferCellPosition): boolean {
-    const sameLine = link.range.start.y === link.range.end.y;
-    const wrappedFromLeft = link.range.start.y < position.y;
-    const wrappedToRight = link.range.end.y > position.y;
-
-    // If the start and end have the same y, then the position must be between start and end x
-    // If not, then handle each case seperately, depending on which way it wraps
-    return ((sameLine && link.range.start.x <= position.x && link.range.end.x >= position.x) ||
-      (wrappedFromLeft && link.range.end.x >= position.x) ||
-      (wrappedToRight && link.range.start.x <= position.x) ||
-      (wrappedFromLeft && wrappedToRight)) &&
-      link.range.start.y <= position.y &&
-      link.range.end.y >= position.y;
+  // private _linkAtPosition(link: ILink, position: IBufferCellPosition): boolean {
+  //   const sameLine = link.range.start.y === link.range.end.y;
+  //   const wrappedFromLeft = link.range.start.y < position.y;
+  //   const wrappedToRight = link.range.end.y > position.y;
+  //
+  //   // If the start and end have the same y, then the position must be between start and end x
+  //   // If not, then handle each case seperately, depending on which way it wraps
+  //   return ((sameLine && link.range.start.x <= position.x && link.range.end.x >= position.x) ||
+  //     (wrappedFromLeft && link.range.end.x >= position.x) ||
+  //     (wrappedToRight && link.range.start.x <= position.x) ||
+  //     (wrappedFromLeft && wrappedToRight)) &&
+  //     link.range.start.y <= position.y &&
+  //     link.range.end.y >= position.y;
+  // }
+  // FIXME: proper test against multiple ranges
+  private _linkAtPosition(link: ILink, pos: IBufferCellPosition): boolean {
+    for (const r of link.ranges) {
+      if (r.end.y >= pos.y && r.end.x >= pos.x && r.start.y <= pos.y && r.start.x <= pos.x) {
+        return true;
+      }
+    }
+    return false;
   }
 
   /**
diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts
index 76aa0c8cfe..15d6c6da1b 100644
--- a/src/browser/Types.d.ts
+++ b/src/browser/Types.d.ts
@@ -268,7 +268,7 @@ interface ILinkProvider {
 }
 
 interface ILink {
-  range: IBufferRange;
+  ranges: IBufferRange[];
   text: string;
   decorations?: ILinkDecorations;
   activate(event: MouseEvent, text: string): void;
diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts
index 6ce69483d9..b7809ec85f 100644
--- a/src/common/Types.d.ts
+++ b/src/common/Types.d.ts
@@ -87,6 +87,7 @@ export type IColorRGB = [number, number, number];
 export interface IExtendedAttrs {
   underlineStyle: number;
   underlineColor: number;
+  urlId: number;
   clone(): IExtendedAttrs;
   isEmpty(): boolean;
 }
diff --git a/src/common/buffer/AttributeData.ts b/src/common/buffer/AttributeData.ts
index c7217a2a0c..8173ec9f90 100644
--- a/src/common/buffer/AttributeData.ts
+++ b/src/common/buffer/AttributeData.ts
@@ -130,11 +130,13 @@ export class ExtendedAttrs implements IExtendedAttrs {
     // underline style, NONE is empty
     public underlineStyle: UnderlineStyle = UnderlineStyle.NONE,
     // underline color, -1 is empty (same as FG)
-    public underlineColor: number = -1
+    public underlineColor: number = -1,
+    // url hover id, 0 is empty
+    public urlId = 0
   ) {}
 
   public clone(): IExtendedAttrs {
-    return new ExtendedAttrs(this.underlineStyle, this.underlineColor);
+    return new ExtendedAttrs(this.underlineStyle, this.underlineColor, this.urlId);
   }
 
   /**
@@ -142,6 +144,6 @@ export class ExtendedAttrs implements IExtendedAttrs {
    * that needs to be persistant in the buffer.
    */
   public isEmpty(): boolean {
-    return this.underlineStyle === UnderlineStyle.NONE;
+    return this.underlineStyle === UnderlineStyle.NONE && !this.urlId;
   }
 }
diff --git a/tsconfig.all.json b/tsconfig.all.json
index a4286deba8..a7bd9a91c2 100644
--- a/tsconfig.all.json
+++ b/tsconfig.all.json
@@ -7,6 +7,7 @@
     { "path": "./test/benchmark" },
     { "path": "./addons/xterm-addon-attach/src" },
     { "path": "./addons/xterm-addon-fit/src" },
+    { "path": "./addons/xterm-addon-hyperlinks/src" },
     { "path": "./addons/xterm-addon-ligatures/src" },
     { "path": "./addons/xterm-addon-search/src" },
     { "path": "./addons/xterm-addon-serialize/src" },
diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts
index dfda85831b..da02e95d80 100644
--- a/typings/xterm.d.ts
+++ b/typings/xterm.d.ts
@@ -1114,7 +1114,7 @@ declare module 'xterm' {
     /**
      * The buffer range of the link.
      */
-    range: IBufferRange;
+    ranges: IBufferRange[];
 
     /**
      * The text of the link.