diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for brick-tabular-list
+
+## 0.1.0.0 -- 2023-02-09
+
+* First version with a proper documentation.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,3 @@
+Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/brick-tabular-list.cabal b/brick-tabular-list.cabal
new file mode 100644
--- /dev/null
+++ b/brick-tabular-list.cabal
@@ -0,0 +1,144 @@
+cabal-version:      3.0
+name:               brick-tabular-list
+version:            0.1.0.0
+
+synopsis: Tabular list widgets for brick.
+
+description:
+  This package contains two tabular list widgets for brick.
+
+  == Grid tabular list
+
+  ![ ](grid-tabular-list-01.png) ![ ](grid-tabular-list-02.png) ![ ](grid-tabular-list-03.png)
+
+  == Mixed tabular list
+
+  ![ ](mixed-tabular-list.png)
+
+  == To get started
+
+  * Read "Brick.Widgets.TabularList.Grid" and "Brick.Widgets.TabularList.Mixed".
+  * Run demo programs in demos/ directory. To learn more quickly, modify and run demo programs.
+
+  == Lens support
+
+  If you want to use lens, you should use `OverloadedLabels` extension with generic-lens or optics-core.
+  If you import any tabular list widget, "Data.Generics.Labels" from generic-lens comes for free.
+
+  You can use generic-lens with lens, microlens, or any other lens package with van Laarhoven lens interface.
+
+  For zoom, you have to use van Laarhoven lens because brick supports zoom through microlens.
+
+homepage: https://codeberg.org/amano.kenji/brick-tabular-list
+bug-reports: https://codeberg.org/amano.kenji/brick-tabular-list/issues
+
+author: amano.kenji
+maintainer: amano.kenji@proton.me
+license: 0BSD
+license-file: LICENSE
+
+category: User Interface
+extra-source-files:
+    CHANGELOG.md
+
+extra-doc-files: *.png
+
+source-repository head
+    type: git
+    location: https://codeberg.org/amano.kenji/brick-tabular-list.git
+
+Flag demos
+    Description: Build demonstration programs
+    Default: False
+
+library
+    exposed-modules:
+        Brick.Widgets.TabularList
+      , Brick.Widgets.TabularList.Grid
+      , Brick.Widgets.TabularList.Mixed
+      , Brick.Widgets.TabularList.Types
+    other-modules:
+        Brick.Widgets.TabularList.Internal.Common
+      , Brick.Widgets.TabularList.Internal.Lens
+    build-depends:
+        base ^>=4.15.1.0
+      , brick >=1.5 && <1.7
+      , containers ^>=0.6.4
+      , generic-lens ^>=2.2.1
+      , microlens ^>=0.4.13
+      , optics-core ^>=0.4.1
+      , vty ^>=5.38
+    default-language: Haskell2010
+    hs-source-dirs: src
+
+executable mixed-tabular-list
+    if !flag(demos)
+      buildable: False
+    main-is:          MixedTabularList.hs
+    other-modules:
+        Internal.MixedTabularList
+    build-depends:
+        base ^>=4.15.1.0
+      , brick >=1.5 && <1.7
+      , brick-tabular-list
+      , containers ^>=0.6.4
+      , microlens ^>=0.4.13
+      , optics-core ^>=0.4.1
+      , vty ^>=5.38
+    hs-source-dirs:   demos
+    default-language: Haskell2010
+    ghc-options:      -threaded
+
+executable mixed-tabular-list-vi
+    if !flag(demos)
+      buildable: False
+    main-is:          MixedTabularListVi.hs
+    other-modules:
+        Internal.MixedTabularList
+    build-depends:
+        base ^>=4.15.1.0
+      , brick >=1.5 && <1.7
+      , brick-tabular-list
+      , containers ^>=0.6.4
+      , microlens ^>=0.4.13
+      , optics-core ^>=0.4.1
+      , vty ^>=5.38
+    hs-source-dirs:   demos
+    default-language: Haskell2010
+    ghc-options:      -threaded
+
+executable grid-tabular-list
+    if !flag(demos)
+      buildable: False
+    main-is: GridTabularList.hs
+    other-modules:
+        Internal.GridTabularList
+    build-depends:
+        base ^>=4.15.1.0
+      , brick >=1.5 && <1.7
+      , brick-tabular-list
+      , containers ^>=0.6.4
+      , microlens ^>=0.4.13
+      , optics-core ^>=0.4.1
+      , vty ^>=5.38
+    hs-source-dirs: demos
+    default-language: Haskell2010
+    ghc-options: -threaded
+
+executable grid-tabular-list-vi
+    if !flag(demos)
+      buildable: False
+    main-is: GridTabularListVi.hs
+    other-modules:
+        Internal.GridTabularList
+    build-depends:
+        base ^>=4.15.1.0
+      , brick >=1.5 && <1.7
+      , brick-tabular-list
+      , containers ^>=0.6.4
+      , microlens ^>=0.4.13
+      , optics-core ^>=0.4.1
+      , vty ^>=5.38
+    hs-source-dirs: demos
+    default-language: Haskell2010
+    ghc-options: -threaded
diff --git a/demos/GridTabularList.hs b/demos/GridTabularList.hs
new file mode 100644
--- /dev/null
+++ b/demos/GridTabularList.hs
@@ -0,0 +1,21 @@
+module Main where
+
+import Internal.GridTabularList
+import Brick.Widgets.TabularList.Grid
+
+main :: IO ()
+main = do
+  let msgs = [ "Press Up arrow to go up one item"
+             , "Press Down arrow to go down one item"
+             , "Press PageUp to go up one page"
+             , "Press PageDown to go down one page"
+             , "Press Home to go to the beginning"
+             , "Press End to go to the end" 
+             , " "
+             , "Press Left arrow to go left by one column"
+             , "Press Right arrow to go right by one column"
+             , "Press Ctrl+Home to go to the first column"
+             , "Press Ctrl+End to go to the last column"
+             , "Press Ctrl+PageUp to go left one page of columns"
+             , "Press Ctrl+PageDown to go right one page of columns" ]
+  runMain msgs handleGridListEvent
diff --git a/demos/GridTabularListVi.hs b/demos/GridTabularListVi.hs
new file mode 100644
--- /dev/null
+++ b/demos/GridTabularListVi.hs
@@ -0,0 +1,25 @@
+module Main where
+
+import Internal.GridTabularList
+import Brick.Widgets.TabularList.Grid
+
+main :: IO ()
+main = do
+  let msgs = [ "Press Up arrow or k to go up one item"
+             , "Press Down arrow or j to go down one item"
+             , "Press PageUp or Ctrl+f to go up one page"
+             , "Press PageDown or Ctrl+b to go down one page"
+             , "Press Home or g to go to the beginning"
+             , "Press End or G to go to the end" 
+             , "Press Ctrl+u to go up half page"
+             , "Press Ctrl+d to go down half page"
+             , " "
+             , "Press Left arrow or h to go left by one column"
+             , "Press Right arrow or l to go right by one column"
+             , "Press Ctrl+Home or H to go to the first column"
+             , "Press Ctrl+End or L to go to the last column"
+             , "Press Ctrl+PageUp or Alt+h or Backspace to go left by one page of columns"
+             , "Press Ctrl+PageDown or Alt+l to go right by one page of columns" ]
+  runMain msgs $ \r e -> do
+    handleGridListEvent r e
+    handleGridListEventVi r e
diff --git a/demos/Internal/GridTabularList.hs b/demos/Internal/GridTabularList.hs
new file mode 100644
--- /dev/null
+++ b/demos/Internal/GridTabularList.hs
@@ -0,0 +1,184 @@
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedLabels #-}
+{-# LANGUAGE FlexibleContexts #-}
+module Internal.GridTabularList (
+  runMain
+, theList
+)
+where
+
+import Brick.Widgets.TabularList.Grid hiding (sizes, contents)
+-- base
+import GHC.Generics (Generic)
+import Control.Monad (void)
+-- Third party libraries
+import Optics.Core
+import Data.Sequence (Seq(..))
+import qualified Data.Sequence as S
+-- brick
+import Brick.Main
+import Brick.AttrMap
+import Brick.Types
+import Brick.Widgets.Core
+import Brick.Widgets.Center
+import Brick.Widgets.Border
+import Brick.Util
+import Brick.Widgets.List
+import Brick.Widgets.Border.Style
+import Graphics.Vty (defAttr, Event(..), Key(..), Modifier(..), black, white, blue, red)
+
+data Song = Song {
+  artist :: String
+, title :: String
+, album :: String
+, composer :: String
+, genre :: String
+, time :: Int
+} deriving Generic
+
+data SongCell = StringCell String | TimeCell Int deriving Generic
+
+data Name = TheList deriving (Eq, Ord, Show)
+
+type LibraryList = GridTabularList Name Song SongCell Int String
+type LibraryRenderers = GridRenderers Name Song SongCell Int String
+type LibraryContents = GridContents Name Song SongCell Int String
+type LibraryEventHandler = LibraryRenderers -> Event -> EventM Name LibraryList ()
+
+songs :: Seq Song
+songs = S.fromList $
+  map (\n -> Song ("Artist " <> show n) ("Title " <> show n)
+                  ("Album " <> show n) ("Composer " <> show n) ("Genre " <> show n)
+                  n)
+      [1..1000]
+
+sizes :: GridSizes Int
+sizes = GridSizes {
+  row = S.fromList [12, 11, 11, 14, 11, 7]
+, rowHdr = Just $ VisibleRowHeaders $ \_ rowHs -> (+2) $ maximum $ map (length . show) rowHs
+, colHdr = Just 1
+}
+
+contents :: LibraryContents
+contents = let
+  getCell s 0 = Just $ StringCell $ s ^. #artist
+  getCell s 1 = Just $ StringCell $ s ^. #title
+  getCell s 2 = Just $ StringCell $ s ^. #album
+  getCell s 3 = Just $ StringCell $ s ^. #composer
+  getCell s 4 = Just $ StringCell $ s ^. #genre
+  getCell s 5 = Just $ TimeCell $ s ^. #time
+  getCell _ _ = Nothing
+  getColumnHeader 0 = Just "Artist"
+  getColumnHeader 1 = Just "Title"
+  getColumnHeader 2 = Just "Album"
+  getColumnHeader 3 = Just "Composer"
+  getColumnHeader 4 = Just "Genre"
+  getColumnHeader 5 = Just "Time"
+  getColumnHeader _ = Nothing
+  getRowHeader _ n = Just (n+1)
+  in GridContents {
+    cell = getCell
+  , rowHdr = Just getRowHeader
+  , colHdr = Just getColumnHeader
+  }
+
+data AppState = AppState {
+  libList :: LibraryList
+, libRenderers :: LibraryRenderers
+, listWidth :: Int
+} deriving Generic
+
+handleLibEvent :: LibraryEventHandler -> BrickEvent Name () -> EventM Name AppState ()
+handleLibEvent eh e = do
+  s <- get
+  case e of
+    VtyEvent (EvKey KEsc []) -> halt
+    VtyEvent (EvKey (KChar 'q') []) -> halt
+    VtyEvent (EvKey (KChar 'c') []) -> zoom (#libRenderers . #drawColHdr) $ modify $ \case
+      Nothing -> Just dch
+      Just _ -> Nothing
+    VtyEvent (EvKey (KChar 'r') []) -> zoom (#libRenderers . #drawRowHdr) $ modify $ \case
+      Nothing -> Just drh
+      Just _ -> Nothing
+    VtyEvent (EvKey (KChar '-') []) -> zoom #listWidth $ modify $ max 1 . subtract 1
+    VtyEvent (EvKey (KChar '=') []) -> zoom #listWidth $ modify (+1)
+    VtyEvent e -> zoom #libList $ eh (s ^. #libRenderers) e
+    _ -> return ()
+
+drawUi :: [String] -> AppState -> [Widget Name]
+drawUi msgs s = let
+  theList = renderGridTabularList (s ^. #libRenderers) True (s ^. #libList)
+  in [vCenter $ vBox $ hCenter (padLeftRight 2 $ border $ hLimit (s ^. #listWidth) $ vLimit 15 theList)
+                     : hCenter (str "Press Esc or q to exit" )
+                     : hCenter (str "Press c to toggle column headers")
+                     : hCenter (str "Press r to toggle row headaers")
+                     : hCenter (str "Press - to shorten the list and = to widen the list")
+                     : hCenter (str " ")
+                     : map (hCenter . str) msgs]
+
+columnHdrAttr :: AttrName
+columnHdrAttr = attrName "columnHeader"
+rowHdrAttr :: AttrName
+rowHdrAttr = attrName "rowHeader"
+colSelectedAttr :: AttrName
+colSelectedAttr = attrName "selectedColumn"
+
+getApp :: [String] -> LibraryEventHandler -> App AppState () Name
+getApp msgs eh = App {
+  appDraw = drawUi msgs
+, appChooseCursor = neverShowCursor
+, appHandleEvent = handleLibEvent eh
+, appStartEvent = return ()
+, appAttrMap = const $ attrMap defAttr [ (colSelectedAttr, black `on` white)
+                                       , (columnHdrAttr, fg blue)
+                                       , (rowHdrAttr, fg red)]
+}
+
+drh lf wd (Position i f) row = \case
+  Nothing -> hCenter $ str " "
+  Just rh ->
+    let attrFn = if f
+          then id
+          else withAttr rowHdrAttr
+        rp = if wd > 0 then 0 else 1
+    in attrFn $ padRight (Pad rp) $ padLeft Max (str $ show rh)
+
+dch lf wd (Position i f) = \case
+  Nothing -> hCenter $ str " "
+  Just ch -> let rp = if wd > 0 then 0 else 1 in
+    withAttr columnHdrAttr $ padRight (Pad rp) $ padRight Max (str ch)
+
+renderers :: LibraryRenderers
+renderers = GridRenderers {
+  drawCell = \lf wd gc song mc -> let
+    attrFn = if gc ^. #row % #selected && gc ^. #col % #selected 
+      then withAttr colSelectedAttr
+      else id
+    in attrFn $ case mc of
+      Nothing -> hCenter $ str " "
+      Just (StringCell s) -> let rp = if wd > 0 then 0 else 1 in
+        padRight (Pad rp) $ padRight Max (str s)
+      Just (TimeCell time) -> let
+        (min, sec) = time `divMod` 60
+        time' = case min of
+          0 -> show sec
+          _ -> show min <> ":" <> show sec
+        rp = if wd > 0 then 0 else 1
+        in padRight (Pad rp) $ padRight Max $ str time'
+, drawRowHdr = Just drh
+, drawColHdr = Just dch
+}
+
+theList :: LibraryList
+theList = gridTabularList TheList songs 1 sizes contents
+
+runMain :: [String] -> LibraryEventHandler -> IO ()
+runMain msgs eh = do
+  let appState = AppState {
+        libList = theList
+      , libRenderers = renderers
+      , listWidth = 39
+      }
+  void $ defaultMain (getApp msgs eh) appState
diff --git a/demos/Internal/MixedTabularList.hs b/demos/Internal/MixedTabularList.hs
new file mode 100644
--- /dev/null
+++ b/demos/Internal/MixedTabularList.hs
@@ -0,0 +1,175 @@
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedLabels #-}
+module Internal.MixedTabularList (
+  runMain
+)
+where
+
+import Brick.Widgets.TabularList.Mixed hiding (sizes, contents)
+-- base
+import GHC.Generics (Generic)
+import Control.Monad (void)
+-- Third party libraries
+import Optics.Core
+import Data.Sequence (Seq(..))
+import qualified Data.Sequence as S
+-- brick
+import Brick.Main
+import Brick.AttrMap
+import Brick.Types
+import Brick.Widgets.Core
+import Brick.Widgets.Center
+import Brick.Widgets.Border
+import Brick.Util
+import Brick.Widgets.List
+import Brick.Widgets.Border.Style
+import Graphics.Vty (defAttr, Event(..), Key(..), Modifier(..), black, white, blue, red)
+
+data Song = Song {
+  artist :: String
+, title :: String
+, album :: String
+, time :: Int
+} deriving Generic
+
+data LibraryEntry = LibFolder String | LibSong Song deriving Generic
+
+data LibCell = StringCell String | FolderCell String | TimeCell Int deriving Generic
+
+data Name = TheList deriving (Eq, Ord, Show)
+
+type LibraryList = MixedTabularList Name LibraryEntry LibCell Int String
+type LibraryRenderers = MixedRenderers Name LibraryEntry LibCell Int String
+type LibraryContents = MixedContents LibraryEntry LibCell Int String
+type LibraryEventHandler = Event -> EventM Name LibraryList ()
+
+libraryEntries :: Seq LibraryEntry
+libraryEntries = let
+  folders = [LibFolder "[.]", LibFolder "[..]"]
+  songs = map (\n -> LibSong (Song ("Artist " <> show n) ("Title " <> show n) ("Album " <> show n) n)) [1..1000]
+  in S.fromList $ folders ++ songs
+
+contents :: LibraryContents
+contents = let
+  getCell (LibFolder s) 0 = Just $ FolderCell $ "Folder: " <> s
+  getCell (LibFolder _) _ = Nothing
+  getCell (LibSong s) 0 = Just $ StringCell $ s ^. #artist
+  getCell (LibSong s) 1 = Just $ StringCell $ s ^. #title
+  getCell (LibSong s) 2 = Just $ StringCell $ s ^. #album
+  getCell (LibSong s) 3 = Just $ TimeCell $ s ^. #time
+  getCell (LibSong _) _ = Nothing
+  getColumnHeader 0 = Just "Artist"
+  getColumnHeader 1 = Just "Title"
+  getColumnHeader 2 = Just "Album"
+  getColumnHeader 3 = Just "Time"
+  getColumnHeader _ = Nothing
+  getRowHeader _ n = Just (n+1)
+  in MixedContents {
+    cell = getCell
+  , rowHdr = Just getRowHeader
+  , colHdr = Just getColumnHeader
+  }
+
+data AppState = AppState {
+  libList :: LibraryList
+, libRenderers :: LibraryRenderers
+, listWidth :: Int
+} deriving Generic
+
+handleLibEvent :: LibraryEventHandler -> BrickEvent Name () -> EventM Name AppState ()
+handleLibEvent eh e = case e of
+  VtyEvent (EvKey KEsc []) -> halt
+  VtyEvent (EvKey (KChar 'q') []) -> halt
+  VtyEvent (EvKey (KChar 'c') []) -> zoom (#libRenderers . #drawColHdr) $ modify $ \case
+    Nothing -> Just dch
+    Just _ -> Nothing
+  VtyEvent (EvKey (KChar 'r') []) -> zoom (#libRenderers . #drawRowHdr) $ modify $ \case
+    Nothing -> Just drh
+    Just _ -> Nothing
+  VtyEvent (EvKey (KChar '-') []) -> zoom #listWidth $ modify $ max 1 . subtract 1
+  VtyEvent (EvKey (KChar '=') []) -> zoom #listWidth $ modify (+1)
+  VtyEvent e -> zoom #libList $ eh e
+  _ -> return ()
+  where abc = []
+
+drawUi :: [String] -> AppState -> [Widget Name]
+drawUi msgs s = let
+  theList = renderMixedTabularList (s ^. #libRenderers) True (s ^. #libList)
+  in [vCenter $ vBox $ hCenter (border $ vLimit 15 $ hLimit (s ^. #listWidth) theList)
+                     : hCenter (str "Press Esc or q to exit" )
+                     : hCenter (str "Press c to toggle column headers")
+                     : hCenter (str "Press r to toggle row headaers")
+                     : hCenter (str "Press - to shorten the list and = to widen the list")
+                     : hCenter (str " ")
+                     : map (hCenter . str) msgs ]
+
+columnHdrAttr :: AttrName
+columnHdrAttr = attrName "columnHeader"
+rowHdrAttr :: AttrName
+rowHdrAttr = attrName "rowHeader"
+
+getApp :: [String] -> LibraryEventHandler -> App AppState () Name
+getApp msgs eh = App {
+  appDraw = drawUi msgs
+, appChooseCursor = neverShowCursor
+, appHandleEvent = handleLibEvent eh
+, appStartEvent = return ()
+, appAttrMap = const $ attrMap defAttr [ (listSelectedAttr, black `on` white)
+                                       , (columnHdrAttr, fg blue)
+                                       , (rowHdrAttr, fg red)]
+}
+
+dch lf ci = \case
+  Nothing -> hCenter $ str " "
+  Just ch -> withAttr columnHdrAttr $ padRight Max (str ch) <+> str " "
+
+drh lf wd (Position i f) row = \case
+  Nothing -> hCenter $ str " "
+  Just rh ->
+    let attrFn = if f
+          then id
+          else withAttr rowHdrAttr
+        rp = if wd > 0 then 0 else 1
+    in attrFn $ padRight (Pad rp) $ padLeft Max (str $ show rh)
+
+renderers :: LibraryRenderers
+renderers = MixedRenderers {
+  drawCell = \lf (Position i f) row mc -> case mc of
+    Nothing -> hCenter $ str " "
+    Just (StringCell s) -> padRight Max (str s) <+> str " "
+    Just (FolderCell s) -> padRight Max $ str s
+    Just (TimeCell time) -> let
+      (min, sec) = time `divMod` 60
+      time' = case min of
+        0 -> show sec
+        _ -> show min <> ":" <> show sec
+      in padRight Max (str time') <+> str " "
+, drawRowHdr = Just drh
+, drawColHdr = Just dch
+}
+
+runMain :: [String] -> LibraryEventHandler -> IO ()
+runMain msgs eh = do
+  let sizes = MixedSizes {
+        rowHdr = Just $ VisibleRowHeaders $ \_ rowHs -> (+2) $ maximum $ map (length . show) rowHs
+      , colSizes = AvailWidth $ \aW -> let
+          artist = max 7 $ (aW * 30) `div` 100
+          title = max 6 $ (aW * 30) `div` 100
+          time = 7
+          album = aW - artist - title - time
+          songWidths = [artist, title, album, time]
+          in ColSizes {
+            rowKind = \case
+              LibFolder _ -> [aW]
+              LibSong _ -> songWidths
+          , colHdr = Just (songWidths, 1)
+          }
+      }
+      appState = AppState {
+        libList = mixedTabularList TheList libraryEntries 1 sizes contents
+      , libRenderers = renderers
+      , listWidth = 80
+      }
+  void $ defaultMain (getApp msgs eh) appState
diff --git a/demos/MixedTabularList.hs b/demos/MixedTabularList.hs
new file mode 100644
--- /dev/null
+++ b/demos/MixedTabularList.hs
@@ -0,0 +1,14 @@
+module Main where
+
+import Internal.MixedTabularList
+import Brick.Widgets.TabularList.Mixed
+
+main :: IO ()
+main = do
+  let msgs = [ "Press Up arrow to go up one item"
+             , "Press Down arrow to go down one item"
+             , "Press PageUp to go up one page"
+             , "Press PageDown to go down one page"
+             , "Press Home to go to the beginning"
+             , "Press End to go to the end" ]
+  runMain msgs handleMixedListEvent
diff --git a/demos/MixedTabularListVi.hs b/demos/MixedTabularListVi.hs
new file mode 100644
--- /dev/null
+++ b/demos/MixedTabularListVi.hs
@@ -0,0 +1,18 @@
+module Main where
+
+import Internal.MixedTabularList
+import Brick.Widgets.TabularList.Mixed
+
+main :: IO ()
+main = do
+  let msgs = [ "Press Up arrow or k to go up one item"
+             , "Press Down arrow or j to go down one item"
+             , "Press PageUp or Ctrl+b to go up one page"
+             , "Press PageDown or Ctrl+f to go down one page"
+             , "Press Home or g to go to the beginning"
+             , "Press End or G to go to the end"
+             , "Press Ctrl+u to go up half page"
+             , "Press Ctrl+d to go down half page" ]
+  runMain msgs $ \e -> do
+    handleMixedListEvent e
+    handleMixedListEventVi e
diff --git a/grid-tabular-list-01.png b/grid-tabular-list-01.png
new file mode 100644
Binary files /dev/null and b/grid-tabular-list-01.png differ
diff --git a/grid-tabular-list-02.png b/grid-tabular-list-02.png
new file mode 100644
Binary files /dev/null and b/grid-tabular-list-02.png differ
diff --git a/grid-tabular-list-03.png b/grid-tabular-list-03.png
new file mode 100644
Binary files /dev/null and b/grid-tabular-list-03.png differ
diff --git a/mixed-tabular-list.png b/mixed-tabular-list.png
new file mode 100644
Binary files /dev/null and b/mixed-tabular-list.png differ
diff --git a/src/Brick/Widgets/TabularList.hs b/src/Brick/Widgets/TabularList.hs
new file mode 100644
--- /dev/null
+++ b/src/Brick/Widgets/TabularList.hs
@@ -0,0 +1,62 @@
+-- | This module contains only conceptual documentation.
+module Brick.Widgets.TabularList where
+
+import Brick.Widgets.TabularList.Types
+import Brick.Widgets.Center
+import Brick.Widgets.List
+
+-- * #SharedTraitsOfTabularListWidgets# Shared Traits of Tabular List Widgets
+--
+-- $SharedTraitsOfTabularListWidgets
+-- A tabular list consists of cells, column headers, and row headers. Column headers and row headers are optional.
+-- 'WidthDeficit' is calculated for renderers of list elements that are shrunk to the available width.
+--
+-- Widths for list elements are either fixed or calculated. Heights are always fixed.
+--
+-- Because tabular list widgets build on top of 'GenericList', attributes for 'GenericList' apply to tabular list
+-- widgets if the attributes are defined in your brick application.
+--
+-- It can handle a very large data set if you delete invisible rows from memory and fetch visible rows from a database
+-- (file). For example, SQLite database file can handle a large spreadsheet.
+
+-- * #Rendering# Rendering
+--
+-- $Rendering
+-- A rendering function for cells or row headers is given a space with
+--
+-- > setAvailableSize (width, listItemHeight)
+--
+-- A rendering function for column headers is given a space with
+--
+-- > setAvailableSize (width, columnHeaderHeight)
+--
+-- If the given height for rendering functions is not claimed, the list will look broken.
+--
+-- The given width for rendering functions must be claimed by the returned widget with brick's padding functions or
+-- 'hCenter'. Otherwise, the list will look broken.
+--
+-- The following examples show how a rendering function can claim the available width.
+--
+-- > let rp = if widthDeficit > 0 then 0 else 1
+-- > in padRight (Pad rp) $ padLeft Max content
+--
+-- > padRight Max content <+> str " "
+--
+-- > let lp = if widthDeficit > 0 then 0 else 1
+-- > in padLeft (Pad lp) $ hCenter content
+--
+-- In the examples above, I used padding with one character at the left or the right to introduce gaps between columns.
+-- If 'WidthDeficit' is positive, you may want to remove padding because the element that is being rendered is not
+-- followed or preceded by other columns.
+
+-- * #ListTypeVariables# List Type Variables
+--
+-- $ListTypeVariables
+-- * @__n__@ is the type of the resource name for the list. This is not for column headers because column headers are
+-- above the list. Read [brick user guide](https://github.com/jtdaugherty/brick/blob/master/docs/guide.rst) for more
+-- details.
+-- * @__row__@ is the type of each row.
+-- * @__cell__@ is the type of columns of each row. If you want to differentiate different columns, @__cell__@ should
+-- have a data constructor for each type of column.
+-- * @__rowH__@ is the type of row header.
+-- * @__colH__@ is the type of column header.
diff --git a/src/Brick/Widgets/TabularList/Grid.hs b/src/Brick/Widgets/TabularList/Grid.hs
new file mode 100644
--- /dev/null
+++ b/src/Brick/Widgets/TabularList/Grid.hs
@@ -0,0 +1,385 @@
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE OverloadedLabels #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE NoMonomorphismRestriction #-}
+{-# LANGUAGE TupleSections #-}
+-- | Grid tabular list is a uniform grid that supports cell-by-cell navigation.
+--
+-- ![ ](grid-tabular-list-01.png) ![ ](grid-tabular-list-02.png) ![ ](grid-tabular-list-03.png)
+--
+-- Read [Shared Traits of Tabular List Widgets]("Brick.Widgets.TabularList#g:SharedTraitsOfTabularListWidgets")
+-- before reading further.
+--
+-- Because this list is designed to show an arbitrary number of columns, horizontal scrolling is supported through
+-- cell-by-cell navigation.
+--
+-- Grid tabular list tries to show the current column in the center. If it can't show the current column in the center,
+-- it shows the first column in the left corner or the last column in the right corner.
+--
+-- It should be fast enough to handle a large spreadsheet. It is also suitable for an interface to a database table.
+module Brick.Widgets.TabularList.Grid (
+-- * Data types
+  GridContents(..)
+, GridContext(..)
+, GridRenderers(..)
+, GridSizes(..)
+, GridTabularList(..)
+-- * List construction
+, gridTabularList
+-- * Rendering
+, renderGridTabularList
+-- * Column navigation
+, gridMoveLeft
+, gridMoveRight
+, gridMoveTo
+, gridMoveToBeginning
+, gridMoveToEnd
+, gridMovePageUp
+, gridMovePageDown
+-- * Event handlers
+, handleGridListEvent
+, handleGridListEventVi
+-- * Shared types
+, module Brick.Widgets.TabularList.Types
+) where
+
+import Brick.Widgets.TabularList.Types
+import Brick.Widgets.TabularList.Internal.Common
+import Brick.Widgets.TabularList.Internal.Lens
+-- base
+import GHC.Generics (Generic)
+import Data.Foldable (toList)
+import Control.Monad (unless)
+-- Third party libraries
+import Optics.Core hiding (Empty)
+import qualified Data.Sequence as S
+import Data.Sequence (Seq(..))
+import Data.Generics.Labels
+-- Brick & Vty
+import qualified Brick.Widgets.List as L
+import Brick.Types
+import Brick.Widgets.Core
+import Brick.Widgets.Center
+import Graphics.Vty (Event(..), Key(..), Modifier(..))
+import Brick.Main (lookupViewport)
+
+-- | Functions for getting contents of grid tabular list elements.
+-- See [List Type Variables]("Brick.Widgets.TabularList#g:ListTypeVariables").
+data GridContents n row cell rowH colH = GridContents {
+  cell :: row -> ColumnIndex -> Maybe cell
+, rowHdr :: Maybe (row -> RowIndex -> Maybe rowH)
+, colHdr :: Maybe (ColumnIndex -> Maybe colH)
+} deriving Generic
+
+-- | Context information for grid cells
+data GridContext = GridContext {
+  row :: Position -- ^ Position among rows
+, col :: Position -- ^ Position among columns
+} deriving (Show, Generic)
+
+-- | Rendering functions for elements of grid tabular list. See
+--
+-- * [List Type Variables]("Brick.Widgets.TabularList#g:ListTypeVariables")
+-- * [Rendering]("Brick.Widgets.TabularList#g:Rendering")
+data GridRenderers n row cell rowH colH = GridRenderers {
+  drawCell :: ListFocused -> WidthDeficit -> GridContext -> row -> Maybe cell -> Widget n
+, drawRowHdr :: Maybe (ListFocused -> WidthDeficit -> Position -> row -> Maybe rowH -> Widget n)
+, drawColHdr :: Maybe (ListFocused -> WidthDeficit -> Position -> Maybe colH -> Widget n)
+} deriving Generic
+
+-- | Sizes for elements of grid tabular list.
+-- See [List Type Variables]("Brick.Widgets.TabularList#g:ListTypeVariables").
+data GridSizes rowH = GridSizes {
+  row :: Seq Width -- ^ Widths for column headers and row columns
+, rowHdr :: Maybe (RowHeaderWidth rowH) -- ^ Width for row headers
+, colHdr :: Maybe Height -- ^ Height for column headers
+} deriving Generic
+
+-- | See [List Type Variables]("Brick.Widgets.TabularList#g:ListTypeVariables").
+data GridTabularList n row cell rowH colH = GridTabularList {
+  list :: L.GenericList n Seq row -- ^ The underlying primitive list that comes from brick.
+, sizes :: GridSizes rowH
+, contents :: GridContents n row cell rowH colH
+, currentColumn :: ColumnIndex
+} deriving Generic
+
+-- | Create a grid tabular list
+gridTabularList :: n -- ^ The list name (must be unique)
+  -> Seq row -- ^ The initial list rows
+  -> ListItemHeight
+  -> GridSizes rowH
+  -> GridContents n row cell rowH colH
+  -> GridTabularList n row cell rowH colH
+gridTabularList n rows h sizes contents = GridTabularList {
+  list = L.list n rows h
+, sizes = sizes
+, contents = contents
+, currentColumn = 0
+}
+
+data VisibleColumns =
+  -- | No column is visible
+  NoColumn
+  -- | Only current column is visible
+  | CurrentColumn
+  -- | The first column is shown at the left corner.
+  | AnchoredLeft {
+      right :: Int -- ^ The rightmost column that is visible
+  }
+  -- | The current column is shown in the center
+  | MiddleColumns {
+    -- | The leftmost visible column
+    left :: Int,
+    -- | The rightmost visible column
+    right :: Int,
+    -- | Slide the columns to the left by this offset to show the current column in the center
+    offset :: Int,
+    -- | Total widths of all visible columns.
+    totalWidth :: Int
+  }
+  -- | The last column is shown at the right corner.
+  | AnchoredRight {
+    -- | The leftmost visible column
+    left :: Int,
+    -- | Slide the columns to the left by this offset to show the last column at the right corner.
+    offset :: Int,
+    -- | Total widths of all visible columns.
+    totalWidth :: Int
+  }
+  deriving Show
+
+-- | Calculate visible columns from the width available for columns. If there aren't enough columns to the left side
+-- to show the current column in the center, LeftAnchor is returned. If there are enough columns to the left side of
+-- the current column, then check whether there are enough columns of the right side of the current column to show the
+-- current column in the center. If there are enough columns to the right side, MiddleColumns is returned. If there
+-- aren't enough columns to the right side, then try to calculate the leftmost visible column for AnchoredRight.
+-- If there aren't enough columns to fill the availble width for AnchoredRight, then AnchoredLeft is returned.
+visibleColumns :: GridTabularList n row cell rowH colH -> AvailWidth -> VisibleColumns
+visibleColumns l aW = let curCol = l ^. #currentColumn in
+  case S.splitAt curCol (l ^. #sizes % #row) of
+    (_, Empty) -> NoColumn
+    (left, cW :<| right) -> if aW <= 0
+      then NoColumn
+      else if cW >= aW
+      then CurrentColumn
+      else let
+        lW = (aW - cW) `div` 2
+        rW = aW - lW - cW
+        leftForMiddle (l :|> w) idx accW = if accW+w < lW
+          then leftForMiddle l (idx-1) (accW+w)
+          else rightForMiddle idx (accW+w) right (curCol+1) 0
+        leftForMiddle Empty _ accW = rightForLeft right (curCol+1) (accW+cW)
+        rightForMiddle li lAccW (w :<| r) ri accW = if accW+w < rW
+          then rightForMiddle li lAccW r (ri+1) (accW+w)
+          else MiddleColumns { left = li, right = ri, offset = lAccW-lW, totalWidth = lAccW+cW+accW+w }
+        rightForMiddle _ _ Empty _ accW = leftForRight left (curCol-1) (accW+cW)
+        rightForLeft (w :<| r) idx accW = if accW+w < aW
+          then rightForLeft r (idx+1) (accW+w)
+          else AnchoredLeft idx
+        rightForLeft Empty idx _ = AnchoredLeft (idx-1)
+        leftForRight (l :|> w) idx accW = if accW+w < aW
+          then leftForRight l (idx-1) (accW+w)
+          else AnchoredRight { left = idx, offset = accW+w-aW, totalWidth = accW+w }
+        leftForRight Empty _ _ = AnchoredLeft $ length (l ^. #sizes % #row) - 1
+        in leftForMiddle left (curCol-1) 0
+
+renderColumns :: GridTabularList n row cell rowH colH
+  -> VisibleColumns
+  -> (WidthDeficit -> ColumnIndex -> Width -> Widget n)
+  -> Widget n
+renderColumns l vCs dC = Widget Greedy Fixed $ do
+  c <- getContext
+  let cWs = l ^. #sizes % #row
+      iH = l ^. #list % #listItemHeight
+      curCol = l ^. #currentColumn
+      aW = c^^.availWidthL
+  render $ case vCs of
+    NoColumn -> emptyWidget
+    CurrentColumn -> case S.lookup curCol cWs of
+      Nothing -> error $ "Current column, " <> show curCol <> " is outside the boundary of column widths."
+      Just cW -> dC (max 0 $ cW - aW) curCol aW
+    AnchoredLeft right -> hBox $ zipWith (dC 0) [0..] $ toList $ S.take (right+1) cWs
+    MiddleColumns {..} -> cropLeftBy offset $ setAvailableSize (totalWidth, iH) $
+      hBox $ zipWith (dC 0) [left..] $ toList $ S.take (right-left+1) $ S.drop left cWs
+    AnchoredRight {..} -> cropLeftBy offset $ setAvailableSize (totalWidth, iH) $
+      hBox $ zipWith (dC 0) [left..] $ toList $ S.drop left cWs
+
+-- | Render grid tabular list
+renderGridTabularList :: (Ord n, Show n)
+  => GridRenderers n row cell rowH colH -- ^ Renderers
+  -> ListFocused
+  -> GridTabularList n row cell rowH colH -- ^ The list
+  -> Widget n
+renderGridTabularList r lf l = Widget Greedy Greedy $ do
+  c <- getContext
+  let drawCell = r ^. #drawCell
+      cell = l ^. #contents % #cell
+      list = l ^. #list
+      curCol = l ^. #currentColumn
+      aW = c^^.availWidthL
+      aH = c^^.availHeightL
+      iH = list ^. #listItemHeight
+      wSet = setAvailableSize . (, iH)
+      colHdrRow vCs rhw = case (l ^. #contents % #colHdr, l ^. #sizes % #colHdr, r ^. #drawColHdr) of
+        (Nothing, _, _) -> emptyWidget
+        (_, Nothing, _) -> emptyWidget
+        (_, _, Nothing) -> emptyWidget
+        (Just colH, Just colHdrH, Just dch) -> let
+          wSet = setAvailableSize . (, colHdrH)
+          drawCol wd c w = wSet w $ dch lf wd (Position c (c == curCol)) $ colH c
+          in wSet rhw (fill ' ') <+> renderColumns l vCs drawCol
+      renderRow vCs i f r = let
+        drawCol wd c w = let gc = GridContext (Position i f) (Position c $ c == curCol)
+          in wSet w $ drawCell lf wd gc r $ cell r c
+        in renderColumns l vCs drawCol
+      renderList = let vCs = visibleColumns l aW in
+        render $ colHdrRow vCs 0 <=> L.renderListWithIndex (renderRow vCs) lf list
+      renderHdrList rh rhw drh = let
+        rhw' = min rhw aW
+        rhwd = max 0 $ rhw - aW
+        vCs = visibleColumns l $ aW - rhw'
+        renderHdrRow i f row = wSet rhw' (drh lf rhwd (Position i f) row $ rh row i) <+> renderRow vCs i f row
+        in render $ colHdrRow vCs rhw' <=> L.renderListWithIndex renderHdrRow lf list
+  case (l ^. #contents % #rowHdr, l ^. #sizes % #rowHdr, r ^. #drawRowHdr) of
+    (Nothing, _, _) -> renderList
+    (_, Nothing, _) -> renderList
+    (_, _, Nothing) -> renderList
+    (Just rh, Just (FixedRowHeader w), Just drh) -> renderHdrList rh w drh
+    (Just rh, Just (AvailRowHeader w), Just drh) -> renderHdrList rh (w aW) drh
+    (Just rh, Just (VisibleRowHeaders w), Just drh) -> renderHdrList rh (w aW $ visibleRowHdrs list aH rh) drh
+
+-- | Move to the left by one column.
+gridMoveLeft :: GridTabularList n row cell rowH colH -- ^ The list
+  -> GridTabularList n row cell rowH colH
+gridMoveLeft gl = if null $ gl ^. #list % #listSelected
+  then gl
+  else gl & #currentColumn %~ max 0 . subtract 1
+
+-- | Move to the right by one column.
+gridMoveRight :: GridTabularList n row cell rowH colH -- ^ The list
+  -> GridTabularList n row cell rowH colH
+gridMoveRight gl = if null $ gl ^. #list % #listSelected
+  then gl
+  else gl & #currentColumn %~ min (length (gl ^. #sizes % #row) - 1) . (+1)
+
+-- | Move to the given column index
+gridMoveTo :: ColumnIndex
+  -> GridTabularList n row cell rowH colH -- ^ The list
+  -> GridTabularList n row cell rowH colH
+gridMoveTo n gl = if null $ gl ^. #list % #listSelected
+  then gl
+  else gl & #currentColumn .~ (max 0 . min (length (gl ^. #sizes % #row) - 1)) n
+
+-- | Move to the first column.
+gridMoveToBeginning :: GridTabularList n row cell rowH colH -- ^ The list
+  -> GridTabularList n row cell rowH colH
+gridMoveToBeginning gl = if null $ gl ^. #list % #listSelected
+  then gl
+  else gl & #currentColumn .~ 0
+
+-- | Move to the last column.
+gridMoveToEnd :: GridTabularList n row cell rowH colH -- ^ The list
+  -> GridTabularList n row cell rowH colH 
+gridMoveToEnd gl = if null $ gl ^. #list % #listSelected
+  then gl
+  else gl & #currentColumn .~ length (gl ^. #sizes % #row) - 1
+
+-- | 'GridRenderers' are needed because if row header renderer doesn't exist, width calculation is affected.
+gridMovePage :: Ord n
+  => GridRenderers n row cell rowH colH
+  -> (VisibleColumns -> EventM n (GridTabularList n row cell rowH colH) ())
+  -> EventM n (GridTabularList n row cell rowH colH) ()
+gridMovePage r f = do
+  l <- get
+  unless (null $ l ^. #list % #listSelected) $ do
+    v <- lookupViewport $ l ^. #list % #listName
+    case v of
+      Nothing -> return ()
+      Just vp -> let
+        (aW, aH) = vp ^. #_vpSize
+        rhw = case (l ^. #contents % #rowHdr, l ^. #sizes % #rowHdr, r ^. #drawRowHdr) of
+          (Nothing, _, _) -> 0
+          (_, Nothing, _) -> 0
+          (_, _, Nothing) -> 0
+          (_, Just (FixedRowHeader w), _) -> w
+          (_, Just (AvailRowHeader w), _) -> w aW
+          (Just rowH, Just (VisibleRowHeaders w), _) -> w aW $ visibleRowHdrs (l ^. #list) aH rowH
+        in f $ visibleColumns l $ aW - rhw
+
+-- | Move to the previous page of columns.
+--
+-- 'GridRenderers' are needed because if row header renderer doesn't exist, width calculation is affected.
+gridMovePageUp :: Ord n
+  => GridRenderers n row cell rowH colH -- ^ Renderers
+  -> EventM n (GridTabularList n row cell rowH colH) ()
+gridMovePageUp r = gridMovePage r $ \case
+  NoColumn -> return ()
+  CurrentColumn -> modify gridMoveLeft
+  AnchoredLeft {} -> modify gridMoveToBeginning
+  MiddleColumns {..} -> modify $ gridMoveTo left
+  AnchoredRight {..} -> modify $ gridMoveTo left
+
+-- | Move to the next page of columns.
+--
+-- 'GridRenderers' are needed because if row header renderer doesn't exist, width calculation is affected.
+gridMovePageDown :: Ord n
+  => GridRenderers n row cell rowH colH -- ^ Renderers
+  -> EventM n (GridTabularList n row cell rowH colH) ()
+gridMovePageDown r = gridMovePage r $ \case
+  NoColumn -> return ()
+  CurrentColumn -> modify gridMoveRight
+  AnchoredLeft {..} -> modify $ gridMoveTo right
+  MiddleColumns {..} -> modify $ gridMoveTo right
+  AnchoredRight {} -> modify gridMoveToEnd
+
+-- | Handle events for grid tabular list with navigation keys.
+--
+-- It adds the following keyboard shortcuts to 'L.handleListEvent'.
+--
+-- * Move to the left by one column (Left arrow key)
+-- * Move to the right by one column (Right arrow key)
+-- * Go to the first column (Ctrl+Home)
+-- * Go to the last column (Ctrl+End)
+-- * Move to the previous page of columns (Ctrl+PageUp)
+-- * Move to the next page of columns (Ctrl+PageDown)
+--
+-- 'GridRenderers' are needed because if row header renderer doesn't exist, width calculation is affected.
+handleGridListEvent :: Ord n
+  => GridRenderers n row cell rowH colH -- ^ Renderers
+  -> Event -> EventM n (GridTabularList n row cell rowH colH) ()
+handleGridListEvent r e = case e of
+  EvKey KLeft [] -> modify gridMoveLeft
+  EvKey KRight [] -> modify gridMoveRight
+  EvKey KHome [MCtrl] -> modify gridMoveToBeginning
+  EvKey KEnd [MCtrl] -> modify gridMoveToEnd
+  EvKey KPageUp [MCtrl] -> gridMovePageUp r
+  EvKey KPageDown [MCtrl] -> gridMovePageDown r
+  _ -> zoom #list (L.handleListEvent e)
+
+-- | Handle events for grid tabular list with vim keys.
+--
+-- It adds the following keyboard shortcuts to 'L.handleListEventVi'.
+--
+-- * Move to the left by one column (h)
+-- * Move to the right by one column (l)
+-- * Go to the first column (H)
+-- * Go to the last column (L)
+-- * Move to the previous page of columns (Alt+h)
+-- * Move to the next page of columns (Alt+l)
+--
+-- 'GridRenderers' are needed because if row header renderer doesn't exist, width calculation is affected.
+handleGridListEventVi :: Ord n
+  => GridRenderers n row cell rowH colH -- ^ Renderers
+  -> Event -> EventM n (GridTabularList n row cell rowH colH) ()
+handleGridListEventVi r e = case e of
+  EvKey (KChar 'h') [] -> modify gridMoveLeft
+  EvKey (KChar 'l') [] -> modify gridMoveRight
+  EvKey (KChar 'H') [] -> modify gridMoveToBeginning
+  EvKey (KChar 'L') [] -> modify gridMoveToEnd
+  EvKey (KChar 'h') [MMeta] -> gridMovePageUp r
+  EvKey (KChar 'l') [MMeta] -> gridMovePageDown r
+  EvKey (KChar 'h') [MAlt] -> gridMovePageUp r
+  EvKey (KChar 'l') [MAlt] -> gridMovePageDown r
+  _ -> zoom #list (L.handleListEventVi (\_ -> return ()) e)
diff --git a/src/Brick/Widgets/TabularList/Internal/Common.hs b/src/Brick/Widgets/TabularList/Internal/Common.hs
new file mode 100644
--- /dev/null
+++ b/src/Brick/Widgets/TabularList/Internal/Common.hs
@@ -0,0 +1,42 @@
+module Brick.Widgets.TabularList.Internal.Common (
+  visibleRows
+, visibleRowHdrs
+, visibleRowsAndRowHdrs
+) where
+
+import Brick.Widgets.TabularList.Types
+-- base
+import Data.Maybe (fromMaybe, catMaybes)
+import Data.Foldable (toList)
+-- Third party libraries
+import Lens.Micro
+import Data.Sequence (Seq)
+-- Brick
+import Brick.Widgets.List
+
+type Start = Int
+type AvailHeight = Int
+visibleRowsWithStart :: GenericList n Seq row -> AvailHeight -> ([row], Start)
+visibleRowsWithStart l aH = let
+  idx = fromMaybe 0 (l^.listSelectedL)
+  numPerHeight = case aH `divMod` (l^.listItemHeightL) of
+    (nph, 0) -> nph
+    (nph, _) -> nph + 1
+  start = max 0 $ idx - numPerHeight + 1
+  length = numPerHeight * 2
+  rows = toList $ slice start length $ l^.listElementsL
+  in (rows, start)
+
+-- | Return visible rows, given 'GenericList' and height available for list elements
+visibleRows :: GenericList n Seq row -> AvailHeight -> [row]
+visibleRows l aH = visibleRowsWithStart l aH ^. _1
+
+-- | Return visible row headers, given 'GenericList' and height available for list elements
+visibleRowHdrs :: GenericList n Seq row -> AvailHeight -> (row -> RowIndex -> Maybe rowH) -> [rowH]
+visibleRowHdrs l aH rowH = visibleRowsAndRowHdrs l aH rowH ^. _2
+
+-- | Return visible rows and visible row headers, given 'GenericList', available height, and row header function.
+visibleRowsAndRowHdrs :: GenericList n Seq row -> AvailHeight -> (row -> RowIndex -> Maybe rowH) -> ([row], [rowH])
+visibleRowsAndRowHdrs l aH rowH = let
+  (rows, start) = visibleRowsWithStart l aH
+  in (rows, catMaybes $ zipWith rowH rows [start..])
diff --git a/src/Brick/Widgets/TabularList/Internal/Lens.hs b/src/Brick/Widgets/TabularList/Internal/Lens.hs
new file mode 100644
--- /dev/null
+++ b/src/Brick/Widgets/TabularList/Internal/Lens.hs
@@ -0,0 +1,13 @@
+-- | Alternate operators for microlens.
+--
+-- Since optic operators take precedence, microlens operators need to be given alternate names.
+module Brick.Widgets.TabularList.Internal.Lens (
+  (^^.)
+) where
+
+import Lens.Micro ((^.))
+
+
+(^^.) = (^.)
+{-# INLINE (^^.) #-}
+infixl 8 ^^.
diff --git a/src/Brick/Widgets/TabularList/Mixed.hs b/src/Brick/Widgets/TabularList/Mixed.hs
new file mode 100644
--- /dev/null
+++ b/src/Brick/Widgets/TabularList/Mixed.hs
@@ -0,0 +1,178 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE OverloadedLabels #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE BlockArguments #-}
+{-# LANGUAGE TupleSections #-}
+-- | Mixed tabular list is a list with different kinds of rows.
+--
+-- ![ ](mixed-tabular-list.png)
+--
+-- Read [Shared Traits of Tabular List Widgets]("Brick.Widgets.TabularList#g:SharedTraitsOfTabularListWidgets")
+-- before reading further.
+--
+-- Each row belongs to a row kind which is usually a data constructor of row data type. Because there can be more than
+-- one data constructor in row data type, this list is called mixed tabular list. Each row kind can have a different
+-- number of columns than another row kind.
+--
+-- Cell by cell navigation is not supported. You can navigate row by row.
+--
+-- Because this list is designed to show every column in the available space, horizontal scrolling is not supported.
+module Brick.Widgets.TabularList.Mixed (
+-- * Data types
+  MixedContents(..)
+, MixedRenderers(..)
+, ColSizes(..)
+, CalcColSizes(..)
+, MixedSizes(..)
+, MixedTabularList(..)
+-- * List construction
+, mixedTabularList
+-- * Rendering
+, renderMixedTabularList
+-- * Event handlers
+, handleMixedListEvent
+, handleMixedListEventVi
+-- * Shared types
+, module Brick.Widgets.TabularList.Types
+) where
+
+import Brick.Widgets.TabularList.Types
+import Brick.Widgets.TabularList.Internal.Common
+import Brick.Widgets.TabularList.Internal.Lens
+-- base
+import GHC.Generics (Generic)
+import Data.Maybe (catMaybes, fromMaybe)
+-- Third party libraries
+import Data.Map.Strict (Map)
+import qualified Data.Map.Strict as Map
+import Optics.Core
+import Data.Generics.Labels
+import Data.Sequence (Seq)
+-- Brick & Vty
+import qualified Brick.Widgets.List as L
+import Brick.Types
+import Brick.Widgets.Core
+import Brick.Widgets.Center
+import Graphics.Vty.Input.Events (Event)
+
+-- | Functions for getting contents of mixed tabular list elements.
+-- See [List Type Variables]("Brick.Widgets.TabularList#g:ListTypeVariables").
+data MixedContents row cell rowH colH = MixedContents {
+  cell :: row -> ColumnIndex -> Maybe cell -- ^ Function for obtaining a column of a row
+, rowHdr :: Maybe (row -> RowIndex -> Maybe rowH) -- ^ Function for getting row header
+, colHdr :: Maybe (ColumnIndex -> Maybe colH) -- ^ Function for getting column header
+} deriving Generic
+
+-- | Rendering functions for elements of mixed tabular list. See
+--
+-- * [List Type Variables]("Brick.Widgets.TabularList#g:ListTypeVariables")
+-- * [Rendering]("Brick.Widgets.TabularList#g:Rendering")
+data MixedRenderers n row cell rowH colH = MixedRenderers {
+  drawCell :: ListFocused -> Position -> row -> Maybe cell -> Widget n
+, drawRowHdr :: Maybe (ListFocused -> WidthDeficit -> Position -> row -> Maybe rowH -> Widget n)
+, drawColHdr :: Maybe (ListFocused -> ColumnIndex -> Maybe colH -> Widget n)
+} deriving Generic
+
+-- | Column sizes calculated after row header width is calculated.
+-- See [List Type Variables]("Brick.Widgets.TabularList#g:ListTypeVariables").
+data ColSizes row = ColSizes {
+  -- | Widths for each row kind. Use pattern matching to detect the kind of each row. Usually, a row kind is a data
+  -- constructor of row. The returned widths should be pre-calculated widths. If widths are calculated in this function,
+  -- width calculation will be repeated at every row. If the function is given pre-calculated widths, width calculation
+  -- is done only once.
+  rowKind :: row -> [Width]
+  -- | Widths and height for column headers. Widths and height don't have to be pre-calculated because column headers are
+  -- only drawn once. Because they are drawn once, any calculation done in this field is also done only once.
+, colHdr :: Maybe ([Width], Height)
+} deriving Generic
+
+-- | Function for calculating column sizes.
+-- See [List Type Variables]("Brick.Widgets.TabularList#g:ListTypeVariables").
+data CalcColSizes row =
+  -- | Calculate sizes with the width available after row header width is calculated.
+  AvailWidth (AvailWidth -> ColSizes row)
+  -- | Calculate sizes with visible rows and the width available after row header width is calculated.
+  | VisibleRows ([row] -> AvailWidth -> ColSizes row) deriving Generic
+
+-- | Sizes for elements of mixed tabular list.
+-- See [List Type Variables]("Brick.Widgets.TabularList#g:ListTypeVariables").
+data MixedSizes row rowH = MixedSizes {
+  rowHdr :: Maybe (RowHeaderWidth rowH) -- ^ Width for row headers
+, colSizes :: CalcColSizes row -- ^ Function for calculating column sizes after row header width is calculated
+} deriving Generic
+
+-- | See [List Type Variables]("Brick.Widgets.TabularList#g:ListTypeVariables").
+data MixedTabularList n row cell rowH colH = MixedTabularList {
+  list :: L.GenericList n Seq row -- ^ The underlying primitive list that comes from brick
+, sizes :: MixedSizes row rowH
+, contents :: MixedContents row cell rowH colH
+} deriving Generic
+
+-- | Create a mixed tabular list.
+mixedTabularList :: n -- ^ The name of the list. It must be unique.
+  -> Seq row -- ^ The initial list rows
+  -> ListItemHeight
+  -> MixedSizes row rowH
+  -> MixedContents row cell rowH colH
+  -> MixedTabularList n row cell rowH colH
+mixedTabularList n rows h sizes contents = MixedTabularList {
+  list = L.list n rows h
+, sizes = sizes
+, contents = contents
+}
+
+-- | Render mixed tabular list
+renderMixedTabularList :: (Show n, Ord n)
+  => MixedRenderers n row cell rowH colH -- ^ Renderers
+  -> ListFocused
+  -> MixedTabularList n row cell rowH colH -- ^ The list
+  -> Widget n
+renderMixedTabularList r lf l = Widget Greedy Greedy $ do
+  c <- getContext
+  let drawCell = r ^. #drawCell
+      cell = l ^. #contents % #cell
+      aW = c^^.availWidthL
+      aH = c^^.availHeightL
+      iH = l ^. #list % #listItemHeight
+      wSet = setAvailableSize . (, iH)
+      colHdrRow sizes rhw = case (l ^. #contents % #colHdr, sizes, r ^. #drawColHdr) of
+        (Nothing, _, _) -> emptyWidget
+        (_, Nothing, _) -> emptyWidget
+        (_, _, Nothing) -> emptyWidget
+        (Just colH, Just (colWs, colHdrH), Just dch) -> let
+          wSet = setAvailableSize . (, colHdrH)
+          drawCol ci w = wSet w $ dch lf ci $ colH ci
+          in wSet rhw (fill ' ') <+> hBox (zipWith drawCol [0..] colWs)
+      renderRow wprk i f row = let
+        drawColumn ci w = wSet w $ drawCell lf (Position i f) row $ cell row ci
+        in hBox $ zipWith drawColumn [0..] $ wprk row
+      sizesAfterRowHdr rhw rows = case l ^. #sizes % #colSizes of
+        AvailWidth wf -> wf $ aW - rhw
+        VisibleRows wf -> case rows of
+          Nothing -> wf (visibleRows (l ^. #list) aH) $ aW - rhw
+          Just rows -> wf rows $ aW - rhw
+      renderList = let ColSizes {..} = sizesAfterRowHdr 0 Nothing
+        in render $ colHdrRow colHdr 0 <=> L.renderListWithIndex (renderRow rowKind) lf (l ^. #list)
+      renderHdrList rh rhw drh rows = let
+        rhw' = min rhw aW
+        rwhd = max 0 $ rhw - aW
+        ColSizes {..} = sizesAfterRowHdr rhw' rows
+        renderHdrRow i f r = wSet rhw' (drh lf rwhd (Position i f) r $ rh r i) <+> renderRow rowKind i f r
+        in render $ colHdrRow colHdr rhw' <=> L.renderListWithIndex renderHdrRow lf (l ^. #list)
+  case (l ^. #contents % #rowHdr, l ^. #sizes % #rowHdr, r ^. #drawRowHdr) of
+    (Nothing, _, _) -> renderList
+    (_, Nothing, _) -> renderList
+    (_, _, Nothing) -> renderList
+    (Just rh, Just (FixedRowHeader w), Just drh) -> renderHdrList rh w drh Nothing
+    (Just rh, Just (AvailRowHeader w), Just drh) -> renderHdrList rh (w aW) drh Nothing
+    (Just rh, Just (VisibleRowHeaders w), Just drh) -> let (rs, rHs) = visibleRowsAndRowHdrs (l ^. #list) aH rh
+      in renderHdrList rh (w aW rHs) drh (Just rs)
+
+-- | Handle events for mixed tabular list with navigation keys. This just calls 'L.handleListEvent'.
+handleMixedListEvent :: Ord n => Event -> EventM n (MixedTabularList n row cell rowH colH) ()
+handleMixedListEvent e = zoom #list (L.handleListEvent e)
+
+-- | Handle events for mixed tabular list with vim keys. This just calls 'L.handleListEventVi'.
+handleMixedListEventVi :: Ord n => Event -> EventM n (MixedTabularList n row cell rowH colH) ()
+handleMixedListEventVi e = zoom #list (L.handleListEventVi (\_ -> return ()) e)
diff --git a/src/Brick/Widgets/TabularList/Types.hs b/src/Brick/Widgets/TabularList/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Brick/Widgets/TabularList/Types.hs
@@ -0,0 +1,63 @@
+{-# LANGUAGE DeriveGeneric #-}
+-- | Types shared by tabular list widgets
+--
+-- You don't have to import this module because modules for tabular list widgets re-export this module.
+module Brick.Widgets.TabularList.Types (
+  ColumnIndex
+, RowIndex
+, Index
+, Width
+, Height
+, AvailWidth
+, ListItemHeight
+, Position(..)
+, WidthDeficit
+, ListFocused
+, RowHeaderWidth(..)
+) where
+
+-- base
+import GHC.Generics (Generic)
+
+-- Types used by tabular list widgets
+
+-- | Index of column among columns
+type ColumnIndex = Int
+-- | Index of row among rows
+type RowIndex = Int
+-- | Index of element among elements
+type Index = Int
+-- | Width of a list element
+type Width = Int
+-- | Height of a list element
+type Height = Int
+-- | Available width
+type AvailWidth = Int
+-- | The fixed height for row headers and cells.
+--
+-- If the height of row headers or cells is not this height, then the list will look broken.
+type ListItemHeight = Int
+-- | Linear position for tabular list elements.
+data Position = Position {
+  index :: Index -- ^ Index of item
+, selected :: Bool -- ^ Whether or not the item is selected
+} deriving (Show, Generic)
+-- | > widthDeficit = max 0 $ desiredWidth - availableWidth
+--
+-- It is positive when an element is shrunk to the available width.
+--
+-- If it is positive, you may want to remove paddings in your content because it is not followed or preceded by
+-- other columns.
+type WidthDeficit = Int
+-- | Whether the list is focused in an application
+type ListFocused = Bool
+
+-- | Row header width information
+data RowHeaderWidth rowH =
+  -- | A fixed width for row header.
+  FixedRowHeader Width
+  -- | Calculate row header width with the width available for each row of a list
+  | AvailRowHeader (AvailWidth -> Width)
+  -- | Calculate row header width with visible row headers and the width available for each row of a list.
+  | VisibleRowHeaders (AvailWidth -> [rowH] -> Width) 
+  deriving Generic
