diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,17 @@
 Brick changelog
 ---------------
 
+1.8
+---
+
+API changes:
+* Added `Brick.Widgets.Core.forceAttrAllowStyle`, which is like
+  `forceAttr` but allows styles to be preserved rather than overridden.
+
+Other improvements:
+* The `Brick.Forms` documentation was updated to clarify how attributes
+  get used for form fields.
+
 1.7
 ---
 
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -93,6 +93,7 @@
 | [`codenames-haskell`](https://github.com/VigneshN1997/codenames-haskell) | An implementation of the Codenames game |
 | [`haradict`](https://github.com/srhoulam/haradict) | A TUI Arabic dictionary powered by [ElixirFM](https://github.com/otakar-smrz/elixir-fm) |
 | [`Giter`](https://gitlab.com/refaelsh/giter) | A UI wrapper around Git CLI inspired by [Magit](https://magit.vc/). |
+| [`Brickudoku`](https://github.com/Thecentury/brickudoku) | A hybrid of Tetris and Sudoku |
 
 These third-party packages also extend `brick`:
 
diff --git a/brick.cabal b/brick.cabal
--- a/brick.cabal
+++ b/brick.cabal
@@ -1,5 +1,5 @@
 name:                brick
-version:             1.7
+version:             1.8
 synopsis:            A declarative terminal user interface library
 description:
   Write terminal user interfaces (TUIs) painlessly with 'brick'! You
diff --git a/docs/guide.rst b/docs/guide.rst
--- a/docs/guide.rst
+++ b/docs/guide.rst
@@ -1649,8 +1649,8 @@
 ``Brick.Keybindings.KeyDispatcher``.
 
 The following table compares Brick application design decisions and
-runtime behaviors in a typical application compared to one that uses the
-customizable keybindings API:
+runtime behaviors in a typical application to those of an application
+that uses the customizable keybindings API:
 
 +---------------------+------------------------+-------------------------+
 | **Approach**        | **Before runtime**     | **At runtime**          |
@@ -1745,12 +1745,12 @@
   open and only handled ``QuitEvent`` when the window had been closed.
   This kind of "modal" approach to handling events means that we only
   consider a key to have a collision if it is bound to two or more
-  events that are handled in the same event handler.
+  events that are handled in the same event handling context.
 
-There's also another situation that would be problematic, which is when
-an abstract event like ``QuitEvent`` has a key mapping that
+There's also another situation that would be problematic, which is
+when an abstract event like ``QuitEvent`` has a key mapping that
 collides with a key handler that is bound to a specific key using
-``Brick.Keybindings.KeyDispatcher.onKey`` rather than an event:
+``Brick.Keybindings.KeyDispatcher.onKey`` rather than an abstract event:
 
 .. code:: haskell
 
diff --git a/src/Brick/AttrMap.hs b/src/Brick/AttrMap.hs
--- a/src/Brick/AttrMap.hs
+++ b/src/Brick/AttrMap.hs
@@ -27,6 +27,7 @@
   -- * Construction
   , attrMap
   , forceAttrMap
+  , forceAttrMapAllowStyle
   , attrName
   -- * Inspection
   , attrNameComponents
@@ -78,6 +79,7 @@
 -- | An attribute map which maps 'AttrName' values to 'Attr' values.
 data AttrMap = AttrMap Attr (M.Map AttrName Attr)
              | ForceAttr Attr
+             | ForceAttrAllowStyle Attr AttrMap
              deriving (Show, Generic, NFData)
 
 -- | Create an attribute name from a string.
@@ -103,6 +105,11 @@
 forceAttrMap :: Attr -> AttrMap
 forceAttrMap = ForceAttr
 
+-- | Create an attribute map in which all lookups map to the same
+-- attribute. This is functionally equivalent to @attrMap attr []@.
+forceAttrMapAllowStyle :: Attr -> AttrMap -> AttrMap
+forceAttrMapAllowStyle = ForceAttrAllowStyle
+
 -- | Given an attribute and a map, merge the attribute with the map's
 -- default attribute. If the map is forcing all lookups to a specific
 -- attribute, the forced attribute is returned without merging it with
@@ -122,6 +129,7 @@
 -- @
 mergeWithDefault :: Attr -> AttrMap -> Attr
 mergeWithDefault _ (ForceAttr a) = a
+mergeWithDefault _ (ForceAttrAllowStyle f _) = f
 mergeWithDefault a (AttrMap d _) = combineAttrs d a
 
 -- | Look up the specified attribute name in the map. Map lookups
@@ -148,6 +156,12 @@
 -- @
 attrMapLookup :: AttrName -> AttrMap -> Attr
 attrMapLookup _ (ForceAttr a) = a
+attrMapLookup a (ForceAttrAllowStyle forced m) =
+    -- Look up the attribute in the contained map, then keep only its
+    -- style.
+    let result = attrMapLookup a m
+    in forced { attrStyle = attrStyle forced `combineStyles` attrStyle result
+              }
 attrMapLookup (AttrName []) (AttrMap theDefault _) = theDefault
 attrMapLookup (AttrName ns) (AttrMap theDefault m) =
     let results = mapMaybe (\n -> M.lookup (AttrName n) m) (inits ns)
@@ -156,11 +170,14 @@
 -- | Set the default attribute value in an attribute map.
 setDefaultAttr :: Attr -> AttrMap -> AttrMap
 setDefaultAttr _ (ForceAttr a) = ForceAttr a
+setDefaultAttr newDefault (ForceAttrAllowStyle a m) =
+    ForceAttrAllowStyle a (setDefaultAttr newDefault m)
 setDefaultAttr newDefault (AttrMap _ m) = AttrMap newDefault m
 
 -- | Get the default attribute value in an attribute map.
 getDefaultAttr :: AttrMap -> Attr
 getDefaultAttr (ForceAttr a) = a
+getDefaultAttr (ForceAttrAllowStyle _ m) = getDefaultAttr m
 getDefaultAttr (AttrMap d _) = d
 
 combineAttrs :: Attr -> Attr -> Attr
@@ -185,6 +202,7 @@
 applyAttrMappings :: [(AttrName, Attr)] -> AttrMap -> AttrMap
 applyAttrMappings _ (ForceAttr a) = ForceAttr a
 applyAttrMappings ms (AttrMap d m) = AttrMap d ((M.fromList ms) `M.union` m)
+applyAttrMappings ms (ForceAttrAllowStyle a m) = ForceAttrAllowStyle a (applyAttrMappings ms m)
 
 -- | Update an attribute map such that a lookup of 'ontoName' returns
 -- the attribute value specified by 'fromName'.  This is useful for
diff --git a/src/Brick/Forms.hs b/src/Brick/Forms.hs
--- a/src/Brick/Forms.hs
+++ b/src/Brick/Forms.hs
@@ -359,6 +359,9 @@
 -- | A form field for selecting a single choice from a set of possible
 -- choices in a scrollable list. This uses a 'List' internally.
 --
+-- This field's attributes are governed by those exported from
+-- 'Brick.Widgets.List'.
+--
 -- This field responds to the same input events that a 'List' does.
 listField :: forall s e n a . (Ord n, Show n, Eq a)
           => (s -> Vector a)
@@ -492,6 +495,9 @@
 -- a value. The other editing fields in this module are special cases of
 -- this function.
 --
+-- This field's attributes are governed by those exported from
+-- 'Brick.Widgets.Edit'.
+--
 -- This field responds to all events handled by 'editor', including
 -- mouse events.
 editField :: (Ord n, Show n)
@@ -550,6 +556,9 @@
 -- useful in cases where the user-facing representation of a value
 -- matches the 'Show' representation exactly, such as with 'Int'.
 --
+-- This field's attributes are governed by those exported from
+-- 'Brick.Widgets.Edit'.
+--
 -- This field responds to all events handled by 'editor', including
 -- mouse events.
 editShowableField :: (Ord n, Show n, Read a, Show a)
@@ -570,6 +579,9 @@
 -- user-facing representation of a value matches the 'Show' representation
 -- exactly, such as with 'Int', but you don't want to accept just /any/ 'Int'.
 --
+-- This field's attributes are governed by those exported from
+-- 'Brick.Widgets.Edit'.
+--
 -- This field responds to all events handled by 'editor', including
 -- mouse events.
 editShowableFieldWithValidate :: (Ord n, Show n, Read a, Show a)
@@ -598,6 +610,9 @@
 -- | A form field using an editor to edit a text value. Since the value
 -- is free-form text, it is always valid.
 --
+-- This field's attributes are governed by those exported from
+-- 'Brick.Widgets.Edit'.
+--
 -- This field responds to all events handled by 'editor', including
 -- mouse events.
 editTextField :: (Ord n, Show n)
@@ -620,6 +635,9 @@
 -- value represented as a password. The value is always considered valid
 -- and is always represented with one asterisk per password character.
 --
+-- This field's attributes are governed by those exported from
+-- 'Brick.Widgets.Edit'.
+--
 -- This field responds to all events handled by 'editor', including
 -- mouse events.
 editPasswordField :: (Ord n, Show n)
@@ -644,11 +662,18 @@
 formAttr :: AttrName
 formAttr = attrName "brickForm"
 
--- | The attribute for form input fields with invalid values.
+-- | The attribute for form input fields with invalid values. Note that
+-- this attribute will affect any field considered invalid and will take
+-- priority over any attributes that the field uses to render itself.
 invalidFormInputAttr :: AttrName
 invalidFormInputAttr = formAttr <> attrName "invalidInput"
 
--- | The attribute for form input fields that have the focus.
+-- | The attribute for form input fields that have the focus. Note that
+-- this attribute only affects fields that do not already use their own
+-- attributes when rendering, such as editor- and list-based fields.
+-- Those need to be styled by setting the appropriate attributes; see
+-- the documentation for field constructors to find out which attributes
+-- need to be configured.
 focusedFormInputAttr :: AttrName
 focusedFormInputAttr = formAttr <> attrName "focusedInput"
 
diff --git a/src/Brick/Themes.hs b/src/Brick/Themes.hs
--- a/src/Brick/Themes.hs
+++ b/src/Brick/Themes.hs
@@ -6,8 +6,6 @@
 -- | Support for representing attribute themes and loading and saving
 -- theme customizations in INI-style files.
 --
--- The file format is as follows:
---
 -- Customization files are INI-style files with two sections, both
 -- optional: @"default"@ and @"other"@.
 --
diff --git a/src/Brick/Widgets/Core.hs b/src/Brick/Widgets/Core.hs
--- a/src/Brick/Widgets/Core.hs
+++ b/src/Brick/Widgets/Core.hs
@@ -50,6 +50,7 @@
   , modifyDefAttr
   , withAttr
   , forceAttr
+  , forceAttrAllowStyle
   , overrideAttr
   , updateAttrMap
 
@@ -1028,6 +1029,17 @@
     Widget (hSize p) (vSize p) $ do
         c <- getContext
         withReaderT (ctxAttrMapL .~ (forceAttrMap (attrMapLookup an (c^.ctxAttrMapL)))) (render p)
+
+-- | Like 'forceAttr', except that the style of attribute lookups in the
+-- attribute map is preserved and merged with the forced attribute. This
+-- allows for situations where 'forceAttr' would otherwise ignore style
+-- information that is important to preserve.
+forceAttrAllowStyle :: AttrName -> Widget n -> Widget n
+forceAttrAllowStyle an p =
+    Widget (hSize p) (vSize p) $ do
+        c <- getContext
+        let m = c^.ctxAttrMapL
+        withReaderT (ctxAttrMapL .~ (forceAttrMapAllowStyle (attrMapLookup an m) m)) (render p)
 
 -- | Override the lookup of the attribute name 'targetName' to return
 -- the attribute value associated with 'fromName' when rendering the
diff --git a/src/Brick/Widgets/FileBrowser.hs b/src/Brick/Widgets/FileBrowser.hs
--- a/src/Brick/Widgets/FileBrowser.hs
+++ b/src/Brick/Widgets/FileBrowser.hs
@@ -143,9 +143,10 @@
 where
 
 import qualified Control.Exception as E
-import Control.Monad (forM)
+import Control.Monad (forM, when)
 import Control.Monad.IO.Class (liftIO)
 import Data.Char (toLower, isPrint)
+import Data.Foldable (for_)
 import Data.Maybe (fromMaybe, isJust, fromJust)
 import qualified Data.Foldable as F
 import qualified Data.Text as T
@@ -318,10 +319,7 @@
 selectDirectories i =
     case fileInfoFileType i of
         Just Directory -> True
-        Just SymbolicLink ->
-            case fileInfoLinkTargetType i of
-                Just Directory -> True
-                _ -> False
+        Just SymbolicLink -> fileInfoLinkTargetType i == Just Directory
         _ -> False
 
 -- | Set the filtering function used to determine which entries in
@@ -362,7 +360,7 @@
             Left (_::E.IOException) -> entries
             Right parent -> parent : entries
 
-    return $ (setEntries allEntries b)
+    return $ setEntries allEntries b
                  & fileBrowserWorkingDirectoryL .~ path
                  & fileBrowserExceptionL .~ exc
                  & fileBrowserSelectedFilesL .~ mempty
@@ -492,7 +490,7 @@
 applyFilterAndSearch b =
     let filterMatch = fromMaybe (const True) (b^.fileBrowserEntryFilterL)
         searchMatch = maybe (const True)
-                            (\search i -> (T.toLower search `T.isInfixOf` (T.pack $ toLower <$> fileInfoSanitizedFilename i)))
+                            (\search i -> T.toLower search `T.isInfixOf` T.pack (toLower <$> fileInfoSanitizedFilename i))
                             (b^.fileBrowserSearchStringL)
         match i = filterMatch i && searchMatch i
         matching = filter match $ b^.fileBrowserLatestResultsL
@@ -714,6 +712,9 @@
         _ ->
             zoom fileBrowserEntriesL $ handleListEvent e
 
+markSelected :: FileInfo -> EventM n (FileBrowser n) ()
+markSelected e = fileBrowserSelectedFilesL %= Set.insert (fileInfoFilename e)
+
 -- | If the browser's current entry is selectable according to
 -- @fileBrowserSelectable@, add it to the selection set and return.
 -- If not, and if the entry is a directory or a symlink targeting a
@@ -723,29 +724,16 @@
 maybeSelectCurrentEntry :: EventM n (FileBrowser n) ()
 maybeSelectCurrentEntry = do
     b <- get
-    case fileBrowserCursor b of
-        Nothing -> return ()
-        Just entry ->
-            if fileBrowserSelectable b entry
-            then fileBrowserSelectedFilesL %= Set.insert (fileInfoFilename entry)
-            else case fileInfoFileType entry of
-                Just Directory ->
-                    put =<< (liftIO $ setWorkingDirectory (fileInfoFilePath entry) b)
-                Just SymbolicLink ->
-                    case fileInfoLinkTargetType entry of
-                        Just Directory ->
-                            put =<< (liftIO $ setWorkingDirectory (fileInfoFilePath entry) b)
-                        _ ->
-                            return ()
-                _ ->
-                    return ()
+    for_ (fileBrowserCursor b) $ \entry ->
+        if fileBrowserSelectable b entry
+        then markSelected entry
+        else when (selectDirectories entry) $
+            put =<< liftIO (setWorkingDirectory (fileInfoFilePath entry) b)
 
 selectCurrentEntry :: EventM n (FileBrowser n) ()
 selectCurrentEntry = do
     b <- get
-    case fileBrowserCursor b of
-        Nothing -> return ()
-        Just e -> fileBrowserSelectedFilesL %= Set.insert (fileInfoFilename e)
+    for_ (fileBrowserCursor b) markSelected
 
 -- | Render a file browser. This renders a list of entries in the
 -- working directory, a cursor to select from among the entries, a
@@ -765,7 +753,7 @@
                   -- ^ The browser to render.
                   -> Widget n
 renderFileBrowser foc b =
-    let maxFilenameLength = maximum $ (length . fileInfoFilename) <$> (b^.fileBrowserEntriesL)
+    let maxFilenameLength = maximum $ length . fileInfoFilename <$> (b^.fileBrowserEntriesL)
         cwdHeader = padRight Max $
                     str $ sanitizeFilename $ fileBrowserWorkingDirectory b
         selInfo = case listSelectedElement (b^.fileBrowserEntriesL) of
@@ -789,7 +777,7 @@
                                         then ", " <> prettyFileSize (fileStatusSize stat)
                                         else ""
                         in fileTypeLabel (fileStatusFileType stat) <> maybeSize
-            in txt $ (T.pack $ fileInfoSanitizedFilename i) <> ": " <> label
+            in txt $ T.pack (fileInfoSanitizedFilename i) <> ": " <> label
 
         maybeSearchInfo = case b^.fileBrowserSearchStringL of
             Nothing -> emptyWidget
