diff --git a/README.md b/README.md
index 829dcbd..c60c951 100644
--- a/README.md
+++ b/README.md
@@ -197,8 +197,8 @@ we'd like to include the following features in future versions:
 - [ ] API for creating user autocommands.
 - [ ] Support passing mapping mode in opts.
 - [ ] Support specifying multiple modes as a string like 'nvt'.
-- [ ] Support specifying labels on groups which become which-key group names.
-- [ ] Support progressively building up maps with groups, like:
+- [x] Support specifying labels on groups which become which-key group names.
+- [x] Support progressively building up maps with groups, like:
 
   ```lua
   mapx.group({ prefix = "<leader>t" }, "LSP", function()
diff --git a/lua/mapx/mapper.lua b/lua/mapx/mapper.lua
index 37068d3..5877d5b 100644
--- a/lua/mapx/mapper.lua
+++ b/lua/mapx/mapper.lua
@@ -1,4 +1,6 @@
-local merge = require('mapx.util').merge
+local util = require 'mapx.util'
+local merge = util.merge
+local setMerge = util.setMerge
 local log = require 'mapx.log'
 
 local dbgi = log.dbgi
@@ -11,6 +13,7 @@ local Mapper = {
     script = { script = true },
     expr = { expr = true },
     unique = { unique = true },
+    prefix = { prefix = '<leader>' },
   },
 }
 
@@ -22,12 +25,12 @@ local function expandStringOpts(opts)
   for k, v in pairs(opts) do
     if type(k) == 'number' then
       if Mapper.mapopts[v] then
-        res[v] = true
+        res = merge(res, Mapper.mapopts[v])
         goto continue
       end
       local vsub = type(v) == 'string' and vim.fn.substitute(v, [[^<\|>$]], '', 'g')
       if vsub and Mapper.mapopts[vsub] ~= nil then
-        res[vsub] = true
+        res = merge(res, Mapper.mapopts[vsub])
         goto continue
       end
       table.insert(res, v)
@@ -57,6 +60,7 @@ function Mapper.new()
     luaFuncs = {},
     filetypeMaps = {},
     groupOpts = {},
+    groupMapsType = {},
     groupActive = false,
     whichkey = nil,
   }
@@ -179,6 +183,19 @@ function Mapper:register(config, lhss, rhs, ...)
   if type(lhss) ~= 'table' then
     lhss = { lhss }
   end
+
+  if opts.prefix ~= nil then
+    local new_lhss = {}
+    for _, lhs in ipairs(lhss) do
+      table.insert(new_lhss, opts.prefix .. lhs)
+    end
+    lhss = new_lhss
+    opts.prefix = nil
+  end
+  if self.groupActive then
+    self.groupMapsType[config.mode] = true
+  end
+
   if type(rhs) == 'function' then
     -- TODO: rhs gets inserted multiple times if a filetype mapping is
     -- triggered multiple times
@@ -212,18 +229,38 @@ function Mapper:group(...)
     end
   end
   self.groupOpts = expandStringOpts(self.groupOpts)
+  self.groupOpts = self:normalizeOpts(self.groupOpts)
   dbgi('group', self.groupOpts)
-  local opts = self:normalizeOpts(self.groupOpts)
-  if opts.label ~= nil then
-    error('mapx.group: cannot set label on group: ' .. tostring(opts.label))
+  local opts = self.groupOpts
+  if opts.label ~= nil and opts.prefix == nil then
+    error('mapx.group: cannot set label on group without prefix: ' .. tostring(opts.label))
   end
+  local label = opts.label
+  opts.label = nil
   local prevGroupActive = self.groupActive
+  local prevGroupMapsType = self.groupMapsType
   self.groupActive = true
+  self.groupMapsType = {}
   fn()
+  if opts.prefix ~= nil and label ~= nil then
+    local new_opts = vim.deepcopy(opts)
+    new_opts.name = label
+    new_opts.prefix = nil
+    for k, v in pairs(self.groupMapsType) do
+      if v then
+        self:registerName(k, opts.prefix, new_opts)
+      end
+    end
+  end
   self.groupActive = prevGroupActive
   self.groupOpts = prevOpts
-  if self.groupActive == false and self.whichkey then
-    self.whichkey:flush()
+  self.groupMapsType = setMerge(self.groupMapsType, prevGroupMapsType)
+  self.groupActive = true
+  if self.groupActive == false then
+    if self.whichkey then
+      self.whichkey:flush()
+    end
+    self.groupMapsType = {}
   end
 end
 
diff --git a/lua/mapx/util.lua b/lua/mapx/util.lua
index f4519f1..c9b8bda 100644
--- a/lua/mapx/util.lua
+++ b/lua/mapx/util.lua
@@ -1,5 +1,23 @@
 local M = {}
 
+-- Flat merge of 2 or more tables, supporting a mixture of map-like tables and
+-- list-list tables.
+-- - Right-most arguments take precedence.
+-- - Numeric indices are extended, not replaced.
+-- - No side effects.
+function M.setMerge(...)
+  local res = {}
+  for i = 1, select('#', ...) do
+    local arg = select(i, ...)
+    for k, v in pairs(arg) do
+      if v then
+        res[k] = true
+      end
+    end
+  end
+  return res
+end
+
 -- Flat merge of 2 or more tables, supporting a mixture of map-like tables and
 -- list-list tables.
 -- - Right-most arguments take precedence.
@@ -13,6 +31,8 @@ function M.merge(...)
       for k, v in pairs(arg) do
         if type(k) == 'number' then
           table.insert(res, v)
+        elseif k == 'prefix' then
+          res[k] = res[k] ~= nil and res[k] .. v or v
         else
           res[k] = v
         end
