diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -26,6 +26,10 @@
 
 Displays the status of IBus input method.
 
+### Network
+
+Displays the connection status from NetworkManager.
+
 ### Power
 
 Displays the battery status for all the batteries reporting through UPower.
diff --git a/index.html b/index.html
--- a/index.html
+++ b/index.html
@@ -18,6 +18,7 @@
           // Load the necessary widgets
           require([
               'tianbar/power',
+              'tianbar/network',
               'tianbar/time',
               'tianbar/weather'
           ]);
@@ -61,6 +62,7 @@
         <span class="widget-left title">Tianbar</span>
         <!-- Each widget recognizes its own class. -->
         <span class="widget-right widget-time"></span>
+        <span class="widget-right widget-network"></span>
         <span class="widget-right widget-power"></span>
         <span class="widget-right widget-weather"></span>
     </body>
diff --git a/scripts/network.js b/scripts/network.js
new file mode 100644
--- /dev/null
+++ b/scripts/network.js
@@ -0,0 +1,335 @@
+/* jshint esversion: 6 */
+define(['jquery', './dbus'], function ($, dbus) {
+  "use strict";
+
+  const self = {};
+
+  const WIDTH = 25;
+  const HEIGHT = 25;
+
+  const INACTIVE_COLOR = '#999';
+
+  self.settings_command = 'gnome-control-center network &';
+
+  self.widget = function () {
+    return $('.widget-network');
+  };
+
+  self.updated = $.Callbacks();
+
+  const NMActiveConnectionState = {
+    Unknown: 0, // the state of the connection is unknown
+    Activating: 1, // a network connection is being prepared
+    Activated: 2, // there is a connection to the network
+    Deactivating: 3, // the network connection is being torn down and cleaned up
+    Deactivated: 4 // the network connection is disconnected and will be removed
+  };
+
+  const ConnectionType = {
+    WiFi: '802-11-wireless',
+    // TODO: More types
+  };
+
+  // Show the wireless signal strength on the canvas
+  function wireless(context, strength) {
+    const pi = Math.PI;
+
+    const bottomPadding = 6;
+    const maxBars = 4;
+    const barSlope = pi / 5;
+
+    context.save();
+
+    context.lineWidth = 1.5;
+
+    const barCount = Math.round(Math.min(strength, 100) * maxBars / 100);
+    for (var i = 0; i < maxBars; i++) {
+      context.beginPath();
+      if (i >= barCount) {
+        context.strokeStyle = INACTIVE_COLOR;
+      }
+      context.arc(
+        WIDTH / 2, HEIGHT - bottomPadding,
+        4 * i + 2,
+        -barSlope, -(pi - barSlope),
+        true
+      );
+      context.stroke();
+    }
+    context.restore();
+  }
+
+  // Show a generic connection on the canvas
+  function normal(context, color) {
+    const offsetHorizontal = 8;
+    const offsetTopBottom = 5;
+
+    const arrowHeadSize = 5;
+    const arrowBodyWidth = 3;
+    const arrowBodyHeight = 7;
+
+    if (color === undefined) {
+      color = '#000000';
+    }
+    context.save();
+    context.fillStyle = color;
+
+    function arrow() {
+      context.beginPath();
+      context.moveTo(0, 0);
+      context.lineTo(arrowHeadSize, arrowHeadSize);
+      context.lineTo(arrowBodyWidth / 2, arrowHeadSize);
+      context.lineTo(arrowBodyWidth / 2, arrowHeadSize + arrowBodyHeight);
+      context.lineTo(-arrowBodyWidth / 2, arrowHeadSize + arrowBodyHeight);
+      context.lineTo(-arrowBodyWidth / 2, arrowHeadSize);
+      context.lineTo(-arrowHeadSize, arrowHeadSize);
+      context.lineTo(0, 0);
+      context.fill();
+    }
+
+    context.save();
+    context.translate((WIDTH - offsetHorizontal) / 2, offsetTopBottom);
+    arrow();
+    context.restore();
+
+    context.save();
+    context.translate((WIDTH + offsetHorizontal) / 2, HEIGHT - offsetTopBottom);
+    context.rotate(Math.PI);
+    arrow();
+    context.restore();
+    context.restore();
+  }
+
+  // Overlay a progress indication on the canvas
+  function overlayProgress(context) {
+    context.save();
+    context.lineWidth = 1.5;
+
+    for (var i = 0; i < 3; i++) {
+      context.save();
+      context.globalCompositeOperation = 'destination-out';
+      context.fillStyle = 'rgba(255, 255, 255, 0.9)';
+      context.beginPath();
+      context.arc(WIDTH - 1 - 4 * i, HEIGHT - 9, 3, 0, 2 * Math.PI, true);
+      context.fill();
+      context.restore();
+
+      context.beginPath();
+      context.arc(WIDTH - 1 - 4 * i, HEIGHT - 9, 1, 0, 2 * Math.PI, true);
+      context.fill();
+    }
+
+    context.restore();
+  }
+
+  // Overlay a cross indication on the canvas
+  function overlayCross(context) {
+    const crossSize = 5;
+
+    const x1 = WIDTH - 1;
+    const x2 = x1 - crossSize;
+    const y1 = HEIGHT - 5;
+    const y2 = y1 - 5;
+
+    context.save();
+    context.lineWidth = 1.5;
+
+    context.beginPath();
+    context.moveTo(x1, y1);
+    context.lineTo(x2, y2);
+    context.stroke();
+
+    context.beginPath();
+    context.moveTo(x1, y2);
+    context.lineTo(x2, y1);
+    context.stroke();
+
+    context.restore();
+  }
+
+  // Overlay a question indication on the canvas
+  function overlayQuestion(context) {
+    context.save();
+
+    context.font = '10px sans';
+
+    context.fillText('?', WIDTH - 4, HEIGHT - 5);
+
+    context.restore();
+  }
+
+  // Display the network status
+  self.display = function () {
+    const widget = self.widget();
+
+    const canvas = $('<canvas />').attr({
+      width: WIDTH,
+      height: HEIGHT
+    });
+
+    widget.empty();
+    widget.append(canvas);
+
+    const context = canvas[0].getContext('2d');
+
+    const isConnected = self.connection.type !== null;
+    const isWireless = self.connection.type == ConnectionType.WiFi;
+
+    if (!isConnected) {
+      normal(context, INACTIVE_COLOR);
+    } else if (isWireless) {
+      wireless(context, self.connection.strength || 0);
+    } else {
+      normal(context);
+    }
+
+    switch (self.connection.state) {
+      case NMActiveConnectionState.Unknown:
+        overlayQuestion(context);
+        break;
+      case NMActiveConnectionState.Activating:
+        overlayProgress(context);
+        break;
+      case NMActiveConnectionState.Activated:
+        // Normal state - nothing to do
+        break;
+      case NMActiveConnectionState.Deactivating:
+        overlayCross(context);
+        break;
+      case NMActiveConnectionState.Deactivated:
+        overlayCross(context);
+        break;
+      default:
+        overlayQuestion(context);
+    }
+
+    widget.attr('title', self.connection.name);
+
+    self.updated.fire();
+  };
+
+  self.connection = {
+    id: null,
+  };
+
+  // Remove stale information about the connection
+  function reset_connection() {
+    self.connection.name = null;
+    self.connection.type = null;
+    self.connection.state = NMActiveConnectionState.Unknown;
+    self.connection.strength = null;
+  }
+  reset_connection();
+
+  const get_nm_property = (path, iface, prop) => dbus.system.getProperty(
+    'org.freedesktop.NetworkManager', path, iface, prop);
+
+  function watch_properties(path) {
+    // NetworkManager doesn't emit PropertiesChanged signal on the
+    // obj.freedesktop.DBus.Properties interface, but on its own.
+    return dbus.system.listen({
+      path: path,
+      member: 'PropertiesChanged'
+    });
+  }
+
+  function refresh_connection_object(conn, conn_object) {
+    if (conn != self.connection.id ||
+      conn_object != self.connection.specific_object) {
+      // Stale signal from an old connection or device
+      return;
+    }
+
+    const get_obj_property = (iface, prop) => get_nm_property(conn_object, iface, prop);
+
+    if (/AccessPoint/.test(conn_object)) {
+      $.when(
+        get_obj_property('org.freedesktop.NetworkManager.AccessPoint', 'Strength')
+      ).done(function (strength) {
+        self.connection.strength = strength;
+        self.display();
+      });
+    }
+  }
+
+  function refresh_connection(conn) {
+    if (conn != self.connection.id) {
+      // Stale signal from an old connection which isn't the one we want
+      // anymore
+      return;
+    }
+
+    const get_conn_property = (prop) => get_nm_property(
+      conn,
+      'org.freedesktop.NetworkManager.Connection.Active',
+      prop
+    );
+
+    $.when(
+      get_conn_property('Id'),
+      get_conn_property('Type'),
+      get_conn_property('State'),
+      get_conn_property('SpecificObject')
+    ).done(function (conn_name, conn_type, conn_state, conn_object) {
+      self.connection.name = conn_name;
+      self.connection.type = conn_type;
+      self.connection.state = conn_state;
+      self.connection.specific_object = conn_object;
+
+      if (conn_object != '/') {
+        watch_properties(conn_object).then(function (evt) {
+          evt.add(function () {
+            refresh_connection_object(conn, conn_object); });
+        });
+        refresh_connection_object(conn, conn_object);
+      }
+
+      self.display();
+    });
+  }
+
+  function subscribe_to_connection(conn) {
+    if (conn == '/') {
+      // No connection
+      self.connection.type = null;
+      self.connection.state = NMActiveConnectionState.Deactivated;
+      self.display();
+    } else {
+      watch_properties(conn).then(function (evt) {
+        evt.add(function () { refresh_connection(conn); });
+      });
+      refresh_connection(conn);
+    }
+  }
+
+  self.refresh = function () {
+    get_nm_property(
+      '/org/freedesktop/NetworkManager',
+      'org.freedesktop.NetworkManager',
+      'PrimaryConnection'
+    ).done(function (conn) {
+      if (self.connection.id != conn) {
+        self.connection.id = conn;
+        reset_connection();
+        subscribe_to_connection(conn);
+      }
+    });
+  };
+
+  $(document).ready(function () {
+    self.widget().click(function () {
+      $.ajax('tianbar:///execute', {
+        data: {
+          command: self.settings_command
+        }
+      });
+    });
+
+    watch_properties('/org/freedesktop/NetworkManager').then(function (evt) {
+      evt.add(self.refresh);
+    });
+    self.refresh();
+  });
+
+  return self;
+});
diff --git a/scripts/volume.js b/scripts/volume.js
--- a/scripts/volume.js
+++ b/scripts/volume.js
@@ -92,7 +92,7 @@
       widget.click(function () {
         $.ajax('tianbar:///execute', {
           data: {
-            command: 'gnome-control-center sound &'
+            command: self.settings_command
           }
         });
       });
diff --git a/tianbar.cabal b/tianbar.cabal
--- a/tianbar.cabal
+++ b/tianbar.cabal
@@ -1,5 +1,5 @@
 name:                tianbar
-version:             1.0.1.0
+version:             1.0.2.0
 synopsis:            A desktop bar based on WebKit
 description:
   A desktop bar using WebKit for rendering as much as possible.
@@ -16,6 +16,7 @@
                    , index.html
                    , scripts/dbus.js
                    , scripts/ibus.js
+                   , scripts/network.js
                    , scripts/power.js
                    , scripts/socket.js
                    , scripts/tianbar.js
