diff --git a/demo/Main.hs b/demo/Main.hs
new file mode 100644
index 0000000000000000000000000000000000000000..4952029907bb9f59e2c1be33c0ba8f36c118b0b8
--- /dev/null
+++ b/demo/Main.hs
@@ -0,0 +1,66 @@
+{-|
+Module      :  Main
+Copyright   :  Jan Hamal Dvořák
+License     :  AGPL-3
+
+Maintainer  :  mordae@anilinux.org
+Stability   :  unstable
+Portability :  non-portable (ghc)
+-}
+
+module Main ( main
+            )
+where
+  import BasePrelude
+
+  import Hikaru.Demo (makeDemo)
+
+
+  data Options
+    = Options
+      { optMainFunc    :: Options -> IO ()
+      , optConf        :: String
+      }
+
+
+  mainFunc :: Parser (Options -> IO ())
+  mainFunc = flag' mainVersion ( long "version"
+                                 <> short 'V'
+                                 <> help "Show version information"
+                                 <> hidden
+                               )
+             <|> pure mainWebsite
+
+
+  options :: Parser Options
+  options = Options <$> mainFunc
+                    <*> strOption ( long "config"
+                                    <> short 'C'
+                                    <> help "Configuration file to read"
+                                    <> value "byrocraft.cfg"
+                                  )
+
+
+  main :: IO ()
+  main = do
+    opts <- execParser $
+      info (helper *> options)
+           ( fullDesc
+             <> progDesc "Collaborative Administration Tool"
+             <> footer "Report bugs at <http://github.com/mordae/byrocraft/issues>."
+           )
+
+    optMainFunc opts opts
+
+
+  mainVersion :: Options -> IO ()
+  mainVersion _opts = do
+    prog <- getProgName
+    putStrLn $ prog <> " " <> packageVersion
+
+
+  mainWebsite :: Options -> IO ()
+  mainWebsite Options{optConf} = serve =<< load [ Required optConf ]
+
+
+-- vim:set ft=haskell sw=2 ts=2 et:
diff --git a/hikaru.cabal b/hikaru.cabal
index 13ef444d54fa11c85cd5fc7756fb74d2b7ee8834..0cd8be6348ecc4b043d2887b079c2c42c321e074 100644
--- a/hikaru.cabal
+++ b/hikaru.cabal
@@ -4,7 +4,7 @@ cabal-version: 1.12
 --
 -- see: https://github.com/sol/hpack
 --
--- hash: 789872e5159353725050ec2764d9951be0fbf0c39bb684307f281650bb6fd9a8
+-- hash: 29d0cf827300a88d2c302f25025d2f8d70bf6580c115003871c8397c80211d86
 
 name:           hikaru
 version:        0.1.0.0
@@ -47,7 +47,7 @@ library
       Paths_hikaru
   hs-source-dirs:
       lib
-  default-extensions: BlockArguments DataKinds DefaultSignatures FlexibleInstances GADTs GeneralizedNewtypeDeriving KindSignatures LambdaCase MultiParamTypeClasses NamedFieldPuns NoImplicitPrelude OverloadedStrings RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving UndecidableInstances
+  default-extensions: BlockArguments DataKinds DefaultSignatures DeriveGeneric FlexibleInstances GADTs GeneralizedNewtypeDeriving KindSignatures LambdaCase MultiParamTypeClasses NamedFieldPuns NoImplicitPrelude OverloadedStrings RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving UndecidableInstances
   ghc-options: -Wall -Wcompat
   build-depends:
       aeson >=1.4 && <1.5
@@ -70,3 +70,43 @@ library
     , wai >=3.2 && <3.3
     , wai-extra >=3.0 && <3.1
   default-language: Haskell2010
+
+test-suite spec
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  other-modules:
+      Hikaru.Demo
+      Hikaru.DemoSpec
+      Hikaru.FormSpec
+      Hikaru.Test
+      Paths_hikaru
+  hs-source-dirs:
+      test
+  default-extensions: BlockArguments DataKinds DefaultSignatures DeriveGeneric FlexibleInstances GADTs GeneralizedNewtypeDeriving KindSignatures LambdaCase MultiParamTypeClasses NamedFieldPuns NoImplicitPrelude OverloadedStrings RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving UndecidableInstances
+  ghc-options: -Wall -Wcompat -threaded -rtsopts -with-rtsopts=-N
+  cpp-options: -DTEST
+  build-tool-depends:
+      hspec-discover:hspec-discover
+  build-depends:
+      aeson >=1.4 && <1.5
+    , base >=4.13 && <4.14
+    , base-prelude >=1.3 && <1.4
+    , binary >=0.8 && <0.9
+    , bytestring >=0.10 && <0.11
+    , case-insensitive >=1.2 && <1.3
+    , containers >=0.6 && <0.7
+    , cookie >=0.4 && <0.5
+    , cryptonite >=0.26 && <0.27
+    , hikaru
+    , hspec
+    , http-types >=0.12 && <0.13
+    , lucid >=2.9 && <2.10
+    , mtl >=2.2 && <2.3
+    , resourcet >=1.2 && <1.3
+    , string-conversions >=0.4 && <0.5
+    , text >=1.2 && <1.3
+    , text-icu >=0.7 && <0.8
+    , time >=1.9 && <1.10
+    , wai >=3.2 && <3.3
+    , wai-extra >=3.0 && <3.1
+  default-language: Haskell2010
diff --git a/lib/Hikaru/Action.hs b/lib/Hikaru/Action.hs
index 37791519339a6a2b19c4e6efb8f9d81c8ad6dc96..b1742c016c34afca09218ba453df6e774f4db2bd 100644
--- a/lib/Hikaru/Action.hs
+++ b/lib/Hikaru/Action.hs
@@ -855,7 +855,7 @@ where
 
   -- |
   -- Default @Content-Type@ to @text/html; charset=utf8@
-  -- and set the response body to the provided byte string.
+  -- and set the response body to the rendering of provided HTML markup.
   --
   sendHTML :: (MonadAction m) => HtmlT m a -> m ()
   sendHTML html = do
diff --git a/lib/Hikaru/Form.hs b/lib/Hikaru/Form.hs
index d255e9fea1367f7f1810a479d0744f1f43997e2d..e1153f81d39c78bdd374ecebc6414fcb5787f454 100644
--- a/lib/Hikaru/Form.hs
+++ b/lib/Hikaru/Form.hs
@@ -7,589 +7,777 @@ Maintainer  :  mordae@anilinux.org
 Stability   :  unstable
 Portability :  non-portable (ghc)
 
-This module provides applicative form handling.
+This module provides tools for building localized HTML forms
+with server-side validation.
+
+There are three important steps involved:
+
+1. A 'Form' needs to be defined first. For that you need to supply the
+   underlying data type as well as high-level description of the form
+   elements.
+
+2. It needs be fed some request data. You control what data by using
+   'newForm', 'getForm' or 'postForm'.
+
+3. It needs to be rendered as HTML. This module will leave you with a
+   high-level form 'View', but you need to take care of the rendering
+   yourself.
+
 -}
 
 module Hikaru.Form
-  ( MonadCsrf(..)
-  , View(..)
-  , FormNote(..)
-  , NoteLevel(..)
-  , Form
-  , FieldT
-  , newForm
+  (
+  -- * Using Forms
+  -- |
+  -- Running a form generally means turning the request data into an updated
+  -- form view and when validation succeeds also the resulting object.
+  --
+    newForm
   , getForm
   , postForm
-  , hiddenField
-  , hiddenField'
-  , csrfTokenField
-  , inputField
-  , inputField'
-  , textArea
-  , textArea'
-  , selectField
-  , selectField'
-  , selectFieldEnum
-  , selectFieldEnum'
-  , multiSelectField
-  , multiSelectField'
-  , opt
-  , req
-  , processValue
-  , whenChecking
-  , fieldShouldCheck
-  , fieldValue
-  , addNote
-  , addAttribute
-  , hasErrors
-  )
-where
-  import BasePrelude
+  , runForm
 
-  import Control.Monad.Reader
-  import Control.Monad.State
-  import Data.Text (Text, strip)
-  import Hikaru.Action
-  import Hikaru.CSRF
-  import Hikaru.Types
+  -- * Building Forms
+  -- |
+  -- Forms are constructed using a simple DSL described lower.
+  --
+  , Form
+  , FormT
 
+  -- ** Elements
+  , element
+  , hidden
 
-  -- Form Types --------------------------------------------------------------
+  -- ** Controls
+  , ElementT
+  , ControlT
 
+  -- *** Input
+  , input
+  , input'
+  , placeholder
 
-  data View l
-    = FormFields
-      { viewFields     :: [View l]
-      , viewNotes      :: [FormNote l]
-      }
-    | HiddenField
-      { viewName       :: Text
-      , viewValue      :: Maybe Text
-      , viewNotes      :: [FormNote l]
-      , viewAttrs      :: [(Text, Dynamic)]
-      }
-    | InputField
-      { viewName       :: Text
-      , viewLabel      :: l
-      , viewValue      :: Maybe Text
-      , viewNotes      :: [FormNote l]
-      , viewAttrs      :: [(Text, Dynamic)]
-      }
-    | TextArea
-      { viewName       :: Text
-      , viewLabel      :: l
-      , viewValue      :: Maybe Text
-      , viewNotes      :: [FormNote l]
-      , viewAttrs      :: [(Text, Dynamic)]
-      }
-    | SelectField
-      { viewName       :: Text
-      , viewLabel      :: l
-      , viewOptions    :: [(Text, l, Bool)]
-      , viewNotes      :: [FormNote l]
-      , viewAttrs      :: [(Text, Dynamic)]
-      , viewMulti      :: Bool
-      }
+  -- *** Select
+  , select
+  , options
+  , optionsFromValues
+  , optionsFromEnum
 
-  instance Semigroup (View l) where
-    FormFields [] [] <> view = view
-    view <> FormFields [] [] = view
+  -- ** Validation
+  , validate
+  , check
+  , note
 
-    FormFields fs1 ns1 <> FormFields fs2 ns2
-      = FormFields (fs1 <> fs2) (ns1 <> ns2)
+  -- ** Rendering Hints
+  , hint
 
-    FormFields fs ns <> view
-      = FormFields (fs <> [view]) ns
+  -- ** Debugging
+  , dumpForm
+  , dumpControl
 
-    view <> FormFields fs ns
-      = FormFields ([view] <> fs) ns
+  -- * Rendering Forms
+  , View(..)
+  , Element(..)
+  , Control(..)
+  , Note(..)
+  , Field(..)
+  , FieldTag(..)
+  , Option(..)
+  , ToOption(..)
+  , Selectable(..)
+
+  -- ** Localization
+  , FormMessage(..)
+  , FromFormMessage(..)
+  )
+where
+  import BasePrelude hiding (Option, Control)
 
-    v1 <> v2 = FormFields ([v1, v2]) []
+  import Control.Monad.Reader (ReaderT, runReaderT, ask)
+  import Control.Monad.State (StateT, runStateT, execStateT, modify, get)
+  import Control.Monad.Trans (MonadTrans, lift)
+  import Data.Text (Text, strip)
+  import Hikaru.Action
+  import Hikaru.Localize
+  import Hikaru.Types
 
-    {-# INLINE (<>) #-}
 
-  instance Monoid (View l) where
-    mempty = FormFields [] []
-    {-# INLINE mempty #-}
+  data Env o
+    = Env
+      { envPrefix      :: Text
+      , envParams      :: Maybe [(Text, Text)]
+      , envFiles       :: Maybe [(Text, FilePath)]
+      , envValue       :: Maybe o
+      , envValidate    :: Bool
+      }
+    deriving (Show)
 
 
   -- |
-  -- TODO
+  -- Root of the generated form.
   --
-  data FormNote l
-    = FormNote
-      { noteLevel      :: NoteLevel
-      , noteLabel      :: l
+  -- Can be used to render the form as HTML.
+  -- Roughly corresponds to the @\<form\>@ element.
+  --
+  data View l
+    = View
+      { viewElements   :: [Element l]
+      , viewControls   :: [Control l]
       }
-    deriving (Eq, Ord)
+    deriving (Show)
 
 
   -- |
-  -- TODO
+  -- Shortcut for a form that produces a value with the same type as was
+  -- the original object. This should be a rule for all the forms.
   --
-  data NoteLevel
-    = NoteError
-    | NoteNeutral
-    | NoteSuccess
-    deriving (Eq, Ord, Show)
-
-  instance Semigroup NoteLevel where
-    (<>) = min
-    {-# INLINE (<>) #-}
-
-  instance Monoid NoteLevel where
-    mempty = NoteSuccess
-    {-# INLINE mempty #-}
-
+  -- Example:
+  --
+  -- @
+  -- addItemForm :: ('MonadAction' m) => 'Form' Messages m AddItem
+  -- addItemForm = do
+  --   AddItem
+  --     \<$\> 'element' MsgItemName do
+  --           'input'' "name" itemName
+  --
+  --     \<*\> 'element' MsgItemType do
+  --           'select' "type" itemType do
+  --             'optionsFromEnum' MsgItemType
+  -- @
+  --
+  type Form l m o = FormT l o m o
 
-  newtype FormT l m a
+  -- |
+  -- Applicative functor for form construction.
+  --
+  newtype FormT l o m a
     = FormT
-      { unFormT        :: ReaderT (Env l) (StateT (View l) m) a
+      { runFormT       :: ReaderT (Env o) (StateT (View l) m) (Maybe a)
       }
-    deriving (MonadIO, Monad, Applicative, Functor)
 
-  deriving instance (Monad m) => MonadReader (Env l) (FormT l m)
-  deriving instance (Monad m) => MonadState (View l) (FormT l m)
+  instance (Monad m) => Functor (FormT l o m) where
+    fmap f FormT{..} = FormT do
+      x <- runFormT
+      return $ fmap f x
+    {-# INLINE fmap #-}
+
+  instance (Monad m) => Applicative (FormT l o m) where
+    pure x = FormT $ pure $ Just x
+    {-# INLINE pure #-}
+
+    l <*> r = FormT do
+      l' <- runFormT l
+      r' <- runFormT r
+      return $ l' <*> r'
+    {-# INLINE (<*>) #-}
 
-  instance MonadTrans (FormT l) where
-    lift = FormT . lift . lift
-    {-# INLINE lift #-}
 
-  instance (MonadCsrf m) => MonadCsrf (FormT l m)
+  -- |
+  -- A form element that normally corresponds to a single row.
+  --
+  -- An element can be either visible or hidden and can have multiple
+  -- controls. Hidden elements can have only 'input' controls.
+  --
+  data Element l
+    = Element
+      { elemLabel      :: l
+      , elemControls   :: [Control l]
+      }
+    deriving (Show)
 
 
-  newtype Form l m a
-    = Form
-      { unForm         :: FormT l m (Maybe a)
+  -- |
+  -- Element customization context.
+  --
+  newtype ElementT l o m a
+    = ElementT
+      { runElementT    :: ReaderT (Env o) (StateT (Element l) m) (Maybe a)
       }
 
-  instance (Monad m) => Functor (Form l m) where
-    fmap f Form{..} = Form do
-      x <- unForm
+  instance (Monad m) => Functor (ElementT l o m) where
+    fmap f ElementT{..} = ElementT do
+      x <- runElementT
       return $ fmap f x
     {-# INLINE fmap #-}
 
-  instance (Monad m) => Applicative (Form l m) where
-    pure x = Form $ pure $ Just x
+  instance (Monad m) => Applicative (ElementT l o m) where
+    pure x = ElementT $ pure $ Just x
     {-# INLINE pure #-}
 
-    Form{unForm = unFormL} <*> Form{unForm = unFormR} = Form do
-      l <- unFormL
-      r <- unFormR
-      return $ l <*> r
+    l <*> r = ElementT do
+      l' <- runElementT l
+      r' <- runElementT r
+      return $ l' <*> r'
     {-# INLINE (<*>) #-}
 
 
-  data Env l
-    = Env
-      { envPrefix      :: [Text]
-      , envParams      :: [(Text, Text)]
-      , envFiles       :: [(Text, FilePath)]
-      , envCheck       :: Bool
+  -- |
+  -- Form controls extend fields with a name, notes and rendering hints.
+  --
+  data Control l
+    = Control
+      { ctrlName       :: Text
+      , ctrlField      :: Field l
+      , ctrlNotes      :: [Note l]
+      , ctrlHints      :: [Dynamic]
       }
+    deriving (Show)
+
+
+  data ControlState l v m
+    = ControlState
+      { csName         :: Text
+      , csField        :: Field l
+      , csHints        :: [Dynamic]
+      , csValidators   :: [Maybe v -> m [Note l]]
+      , csValue        :: Maybe v
+      }
+
+  instance (Show l, Show v) => Show (ControlState l v m) where
+    show ControlState{..} =
+      mconcat [ "ControlState {"
+              , "csName = " <> show csName
+              , ", csField = " <> show csField
+              , ", csHints = " <> show csHints
+              , ", csValidators = " <> show (length csValidators)
+              , ", csValue = " <> show csValue
+              , "}"
+              ]
 
 
-  newtype FieldT l a m b
-    = FieldT
-      { unFieldT       :: ReaderT Bool (StateT (View l, Maybe a) m) b
+  -- |
+  -- Control customization context.
+  --
+  -- Note that you can access the underlying 'Monad' using 'lift' here.
+  --
+  newtype ControlT (t :: FieldTag) l o v m a
+    = ControlT
+      { runControlT    :: ReaderT (Env o) (StateT (ControlState l v m) m) a
       }
     deriving (MonadIO, Monad, Applicative, Functor)
 
-  instance MonadTrans (FieldT l a) where
-    lift = FieldT . lift . lift
+  instance MonadTrans (ControlT t l o v) where
+    lift = ControlT . lift . lift
     {-# INLINE lift #-}
 
-  instance (MonadCsrf m) => MonadCsrf (FieldT l a m)
+  instance (MonadAction m) => MonadAction (ControlT t l o v m) where
+    getActionEnv = ControlT $ lift $ lift $ getActionEnv
+    {-# INLINE getActionEnv #-}
 
 
   -- |
-  -- Build a fresh form without using any request data.
+  -- Short text with associated severity to be presented along the
+  -- form control. Used to indicate validation results.
   --
-  newForm :: (MonadAction m) => Text -> Form l m a -> m (View l)
-  newForm name = flip execStateT view . flip runReaderT env . unFormT . unForm
-    where
-      view = FormFields [] []
-      env  = Env { envPrefix = [name]
-                 , envParams = []
-                 , envFiles  = []
-                 , envCheck  = False
-                 }
+  data Note l
+    = Note
+      { noteSeverity   :: Severity
+      , noteMessage    :: l
+      }
+    deriving (Show)
 
 
   -- |
-  -- Process the form using parameters in the query string.
+  -- Form field types.
   --
-  getForm :: (MonadAction m) => Text -> Form l m a -> m (View l, Maybe a)
-  getForm name form = do
-    params <- getParams
-
-    let view = FormFields [] []
-        env  = Env { envPrefix = [name]
-                   , envParams = filter (("" /=) . strip . snd) params
-                   , envFiles  = []
-                   , envCheck  = True
-                   }
+  -- If you miss checkbox, radio or textarea, remember that you can
+  -- signal the way to render the field using a rendering 'hint'.
+  --
+  data Field l
+    = InputField
+      { fieldType      :: Text
+      , fieldPlacehold :: Maybe l
+      , fieldValue     :: Text
+      }
+    | SelectField
+      { fieldOptions   :: [Option l]
+      }
+    deriving (Show)
 
-    (value, view') <- runStateT (runReaderT (unFormT $ unForm form) env) view
 
-    if hasErrors view'
-       then return (view', Nothing)
-       else return (view', value)
+  -- |
+  -- Tag used to specialize context for the individual control types.
+  --
+  data FieldTag
+    = InputFieldTag
+    | SelectFieldTag
+    deriving (Show, Eq)
 
 
   -- |
-  -- Process the form using parameters in the request body.
+  -- An option for 'select' to choose from.
   --
-  postForm :: (MonadAction m) => Text -> Form l m a -> m (View l, Maybe a)
-  postForm name form = do
-    fields <- getFields
-    files  <- getFiles
+  data Option l
+    = Option
+      { optionLabel    :: l
+      , optionSelected :: Bool
+      , optionValue    :: Text
+      }
+    deriving (Show)
 
-    let view = FormFields [] []
-        env  = Env { envPrefix = [name]
-                   , envParams = filter (("" /=) . strip . snd) fields
-                   , envFiles  = files
-                   , envCheck  = True
-                   }
 
-    (value, view') <- runStateT (runReaderT (unFormT $ unForm form) env) view
+  class ToOption l o where
+    toOption :: o -> Option l
 
-    if hasErrors view'
-       then return (view', Nothing)
-       else return (view', value)
+  instance ToOption l (Option l) where
+    toOption = id
+    {-# INLINE toOption #-}
 
 
   -- |
-  -- TODO
+  -- A class with two overlapping instances used to smoothly handle
+  -- both single-select and multi-select controls.
   --
-  csrfTokenField :: (MonadCsrf m) => l -> Form l m Text
-  csrfTokenField msg = Form do
-    Env{envCheck} <- ask
+  class Selectable a where
+    selectValues  :: [Text] -> Maybe a
+    selectOptions :: a -> [Option l] -> [Option l]
+
+  instance {-# OVERLAPPING #-} (Param a, Eq a) => Selectable a where
+    selectValues [p] = fromParam p
+    selectValues _ps = Nothing
 
-    name' <- makeName "csrftoken"
-    value <- fromMaybe "" <$> formParamMaybe name'
-    valid <- isTokenValid value
-    token <- generateToken
+    selectOptions p = map update
+      where
+        update opt@Option{..} = opt { optionSelected = match optionValue }
+        match x = case fromParam x of
+                    Nothing -> False
+                    Just x' -> p == x'
 
-    let view = HiddenField { viewName  = name'
-                           , viewValue = Just token
-                           , viewNotes = if envCheck && not valid
-                                            then [FormNote NoteError msg]
-                                            else []
-                           , viewAttrs = []
-                           }
+  instance {-# OVERLAPPING #-} (Param a, Eq a) => Selectable [a] where
+    selectValues = Just . mapMaybe fromParam
 
-    modify (<> view)
-    return $ Just token
+    selectOptions ps = map update
+      where
+        update opt@Option{..} = opt { optionSelected = match optionValue }
+        match x = case fromParam x of
+                    Nothing -> False
+                    Just x' -> x' `elem` ps
 
 
   -- |
-  -- TODO
+  -- Default localized messages related to form validation.
+  --
+  -- You might want to wrap this in your message catalog:
   --
-  hiddenField' :: (Monad m, Param a)
-               => Text -> FieldT l a m b -> Form l m a
-  hiddenField' name field = hiddenField name Nothing field
+  -- @
+  -- data Messages
+  --   = MsgForm 'FormMessage'
+  --   | ...
+  --   deriving (Show)
+  --
+  -- instance 'Localizable' Messages where
+  --   'localize' lang (MsgForm msg) = 'localize' lang msg
+  --   ...
+  -- @
+  --
+  data FormMessage
+    = FormMsgFieldRequired
+    deriving (Show)
+
+  instance Localizable FormMessage where
+    -- English strings
+    localize "en" FormMsgFieldRequired = Just "This field is required."
+
+    -- No translation, caller should try a different locale.
+    localize _lang _msg = Nothing
 
 
   -- |
-  -- TODO
+  -- Class used to wrap the 'FormMessage' with your own message type.
   --
-  hiddenField :: (Monad m, Param a)
-              => Text -> Maybe a -> FieldT l a m b -> Form l m a
-  hiddenField name orig field = Form do
-    name' <- makeName name
-    value <- formParamMaybe name'
-
-    let value' = value <|> orig
-        view   = HiddenField { viewName  = name'
-                             , viewValue = toParam <$> value'
-                             , viewNotes = []
-                             , viewAttrs = []
-                             }
+  -- Just point to the constructor within your own message catalog:
+  --
+  -- @
+  -- instance 'FromFormMessage' Messages where
+  --   'fromFormMessage' = MsgForm
+  -- @
+  --
+  class FromFormMessage l where
+    fromFormMessage :: FormMessage -> l
 
-    (view', value'') <- runFieldT field value' view
 
-    modify (<> view')
-    return value''
+  -- Using Forms -------------------------------------------------------------
 
 
   -- |
-  -- TODO
+  -- Build a fresh form without using any request data.
   --
-  inputField' :: (Monad m, Param a)
-              => Text -> l -> FieldT l a m b -> Form l m a
-  inputField' name label field = inputField name label Nothing field
+  -- This function is used when the form is initially presented to the user.
+  -- It makes no sense to validate it, since the user have not entered any
+  -- input data or it has been seeded using a (most probably) valid object.
+  --
+  -- For example:
+  --
+  -- @
+  -- getEditItemR :: Natural -> Action ()
+  -- getEditItemR itemId = do
+  --   item <- getItem itemId
+  --   view <- 'newForm' "editItem" Nothing editItemForm
+  --   'sendHTML' do
+  --     form_ [method_ "POST"] do
+  --       horizontalForm_ view
+  --       horizontalFormButtons_ do
+  --        submitButton_ do
+  --          'lc_' MsgBtnSubmit
+  -- @
+  --
+  newForm :: (Monad m) => Text -> Maybe o -> Form l m o -> m (View l)
+  newForm name orig = flip execStateT view . flip runReaderT env . runFormT
+    where
+      view = View [] []
+      env  = Env { envPrefix   = name
+                 , envParams   = Nothing
+                 , envFiles    = Nothing
+                 , envValue    = orig
+                 , envValidate = False
+                 }
 
 
   -- |
-  -- TODO
+  -- Build the form using query string parameters.
   --
-  inputField :: (Monad m, Param a)
-             => Text -> l -> Maybe a -> FieldT l a m b -> Form l m a
-  inputField name label orig field = Form do
-    name' <- makeName name
-    value <- formParamMaybe name'
-
-    let value' = value <|> orig
-        view   = InputField { viewName  = name'
-                            , viewLabel = label
-                            , viewValue = toParam <$> value'
-                            , viewNotes = []
-                            , viewAttrs = []
-                            }
+  -- The values are validated and corresponding notes are generated.
+  -- In case of success the resulting object is generated as well.
+  --
+  -- See 'postForm' for an example as they are used the same way.
+  --
+  getForm :: (MonadAction m) => Text -> Form l m o -> m (Maybe o, View l)
+  getForm name FormT{..} = do
+    params <- getParams
 
-    (view', value'') <- runFieldT field value' view
+    let view = View [] []
+        env  = Env { envPrefix   = name
+                   , envParams   = Just params
+                   , envFiles    = Nothing
+                   , envValue    = Nothing
+                   , envValidate = True
+                   }
 
-    modify (<> view')
-    return value''
+    runStateT (flip runReaderT env runFormT) view
 
 
   -- |
-  -- TODO
+  -- Build the form using files and files from the submitted form.
+  --
+  -- The values are validated and corresponding notes are generated.
+  -- In case of success the resulting object is generated as well.
+  --
+  -- For example:
+  --
+  -- @
+  -- postEditItemR :: Natural -> Action ()
+  -- postEditItemR itemId = do
+  --   (result, view) <- 'postForm' "editItem" Nothing editItemForm
   --
-  textArea' :: (Monad m, Param a)
-            => Text -> l -> FieldT l a m b -> Form l m a
-  textArea' name label field = textArea name label Nothing field
+  --   case result of
+  --     Nothing -> do
+  --       'sendHTML' do
+  --         form_ [method_ "POST"] do
+  --           horizontalForm_ view
+  --           horizontalFormButtons_ do
+  --            submitButton_ do
+  --              'lc_' MsgBtnSubmit
+  --
+  --     Just edit -> do
+  --       editItem edit
+  --       'redirect' "/items/"
+  -- @
+  --
+  postForm :: (MonadAction m) => Text -> Form l m o -> m (Maybe o, View l)
+  postForm name FormT{..} = do
+    fields <- getFields
+    files  <- getFiles
+
+    let view = View [] []
+        env  = Env { envPrefix   = name
+                   , envParams   = Just fields
+                   , envFiles    = Just files
+                   , envValue    = Nothing
+                   , envValidate = True
+                   }
+
+    runStateT (flip runReaderT env runFormT) view
 
 
   -- |
-  -- TODO
+  -- Build the form using only the supplied values.
+  --
+  -- The values are validated and corresponding notes are generated.
+  -- In case of success the resulting object is generated as well.
   --
-  textArea :: (Monad m, Param a)
-            => Text -> l -> Maybe a -> FieldT l a m b -> Form l m a
-  textArea name label orig field = Form do
-    name' <- makeName name
-    value <- formParamMaybe name'
+  -- This function is included for completeness only.
+  -- You should probably use either 'newForm', 'getForm' or 'postForm'.
+  --
+  runForm :: (Monad m)
+          => Text -> [(Text, Text)] -> [(Text, FilePath)] -> Form l m o
+          -> m (Maybe o, View l)
+  runForm name params files FormT{..} = do
+    let view = View [] []
+        env  = Env { envPrefix   = name
+                   , envParams   = Just params
+                   , envFiles    = Just files
+                   , envValue    = Nothing
+                   , envValidate = True
+                   }
 
-    let value' = value <|> orig
-        view   = TextArea { viewName  = name'
-                          , viewLabel = label
-                          , viewValue = toParam <$> value'
-                          , viewNotes = []
-                          , viewAttrs = []
-                          }
+    runStateT (flip runReaderT env runFormT) view
 
-    (view', value'') <- runFieldT field value' view
 
-    modify (<> view')
-    return value''
+  -- Building Forms ----------------------------------------------------------
 
 
   -- |
-  -- TODO
+  -- Add a new form element with given label.
   --
-  selectField' :: (Monad m, Param a, Eq a)
-               => Text -> l -> (a -> l) -> [a]
-               -> FieldT l a m b -> Form l m a
-  selectField' name label optlabel options field
-    = selectField name label optlabel options Nothing field
+  -- Individual controls are specified using the 'ElementT' monad.
+  -- Normal elements can contain all control types.
+  --
+  element :: (Monad m) => l -> ElementT l o m a -> FormT l o m a
+  element label body = FormT do
+    env <- ask
+
+    (res, new) <- lift $ lift do
+      let base = Element label []
+       in flip runStateT base $ flip runReaderT env $ runElementT body
+
+    modify \view@View{..} ->
+      view { viewElements = viewElements <> [new] }
+
+    return res
 
 
   -- |
+  -- Add a new hidden form control.
+  --
   -- TODO
   --
-  selectField :: (Monad m, Param a, Eq a)
-              => Text -> l -> (a -> l) -> [a] -> Maybe a
-              -> FieldT l a m b -> Form l m a
-  selectField name label optlabel options orig field = Form do
-    name' <- makeName name
-    value <- formParamMaybe name'
+  hidden :: (Monad m, FromFormMessage l, Param v)
+         => Text
+         -> (o -> v)
+         -> ControlT 'InputFieldTag l o v m a
+         -> FormT l o m v
+  hidden name getter body = FormT do
+    env@Env{..} <- ask
+    (val, text) <- getParamOrig name (getter <$> envValue)
 
-    let value' = value <|> orig
-        opts   = [ (toParam x, optlabel x, Just x == value) | x <- options ]
-        view   = SelectField { viewName    = name'
-                             , viewLabel   = label
-                             , viewOptions = opts
-                             , viewNotes   = []
-                             , viewAttrs   = []
-                             , viewMulti   = False
-                             }
+    new <- lift $ lift do
+      let field = InputField "hidden" Nothing text
+          state = ControlState name field [] [] val
+       in flip execStateT state $ flip runReaderT env $ runControlT body
 
-    (view', value'') <- runFieldT field value' view
+    ctrl <- lift $ lift $ buildControl env new
 
-    modify (<> view')
-    return value''
+    modify \view@View{..} ->
+      view { viewControls = viewControls <> [ctrl] }
+
+    return $ csValue new
 
 
   -- |
   -- TODO
   --
-  selectFieldEnum' :: (Monad m, Param a, Eq a, Bounded a, Enum a)
-                   => Text -> l -> (a -> l) -> FieldT l a m b -> Form l m a
-  selectFieldEnum' name label optlabel field
-    = selectFieldEnum name label optlabel Nothing field
+  input :: (Monad m, FromFormMessage l, Param v)
+        => Text
+        -> (o -> v)
+        -> ControlT 'InputFieldTag l o v m a
+        -> ElementT l o m v
+  input name getter body = ElementT do
+    env@Env{..} <- ask
+    (val, text) <- getParamOrig name (getter <$> envValue)
+
+    new <- lift $ lift do
+      let field = InputField "text" Nothing text
+          state = ControlState name field [] [] val
+       in flip execStateT state $ flip runReaderT env $ runControlT body
+
+    ctrl <- lift $ lift $ buildControl env new
+
+    modify \elt@Element{..} ->
+      elt { elemControls = elemControls <> [ctrl] }
+
+    return $ csValue new
 
 
   -- |
-  -- Alternative to the 'selectField' for enumerable, bounded value types.
-  --
-  -- TODO: Example
+  -- TODO
   --
-  selectFieldEnum :: (Monad m, Param a, Eq a, Bounded a, Enum a)
-                  => Text -> l -> (a -> l) -> Maybe a
-                  -> FieldT l a m b -> Form l m a
-  selectFieldEnum name label optlabel orig field
-    = selectField name label optlabel [minBound..maxBound] orig field
+  input' :: (Monad m, FromFormMessage l, Param v)
+         => Text -> (o -> v) -> ElementT l o m v
+  input' name getter = input name getter $ return ()
 
 
   -- |
   -- TODO
   --
-  multiSelectField' :: (Monad m, Param a, Eq a)
-                    => Text -> l -> (a -> l) -> [a]
-                    -> FieldT l [a] m b -> Form l m [a]
-  multiSelectField' name label optlabel options field
-    = multiSelectField name label optlabel options Nothing field
+  placeholder :: (Monad m) => l -> ControlT 'InputFieldTag l o v m ()
+  placeholder ph = ControlT do
+    modify \s@ControlState{..} ->
+      s { csField = csField { fieldPlacehold = Just ph } }
 
 
   -- |
   -- TODO
   --
-  multiSelectField :: (Monad m, Param a, Eq a)
-                   => Text -> l -> (a -> l) -> [a] -> Maybe [a]
-                   -> FieldT l [a] m b -> Form l m [a]
-  multiSelectField name label optlabel options orig field = Form do
-    name'  <- makeName name
-    params <- formParams name'
+  select :: (Monad m, FromFormMessage l, Param v, Selectable v)
+         => Text
+         -> (o -> v)
+         -> ControlT 'SelectFieldTag l o v m a
+         -> ElementT l o m v
+  select name getter body = ElementT do
+    env@Env{..} <- ask
+    val <- getSelectParams name
 
-    let found  = nub $ params <> fromMaybe [] orig
-        opts   = [ (toParam x, optlabel x, x `elem` found) | x <- options ]
-        view   = SelectField { viewName    = name'
-                             , viewLabel   = label
-                             , viewOptions = opts
-                             , viewNotes   = []
-                             , viewAttrs   = []
-                             , viewMulti   = False
-                             }
+    new <- lift $ lift do
+      let val'  = val <|> (getter <$> envValue)
+          field = SelectField []
+          state = ControlState name field [] [] val'
+       in flip execStateT state $ flip runReaderT env $ runControlT body
 
-    (view', found') <- runFieldT field (Just found) view
+    ctrl <- lift $ lift $ buildControl env new
 
-    modify (<> view')
-    return $ found'
+    modify \elt@Element{..} ->
+      elt { elemControls = elemControls <> [ctrl] }
+
+    return $ csValue new
 
 
   -- |
-  -- TODO
+  -- Configure 'select' options directly.
+  --
+  -- TODO: Add an example.
   --
-  opt :: (Monad m) => FieldT l a m ()
-  opt = return ()
+  options :: (Monad m, Param v, Selectable v)
+          => [Option l] -> ControlT 'SelectFieldTag l o v m ()
+  options opts = ControlT do
+    modify \s@ControlState{..} ->
+      let opts' = case csValue of
+                    Nothing  -> opts
+                    Just val -> selectOptions val opts
+       in s { csField = csField { fieldOptions = opts' } }
 
 
   -- |
-  -- TODO
+  -- Configure 'select' options using a list of values.
+  --
+  -- TODO: Add an example.
   --
-  req :: (Monad m) => l -> FieldT l a m ()
-  req label = do
-    whenChecking do
-      value <- fieldValue
-      case value of
-        Nothing -> addNote $ FormNote NoteError label
-        Just _v -> return ()
+  optionsFromValues :: (Monad m, Param v, Selectable v)
+                    => (v -> l) -> [v] -> ControlT 'SelectFieldTag l o v m ()
+  optionsFromValues label vals = options [ Option (label x) False (toParam x)
+                                   | x <- vals
+                                   ]
 
 
   -- |
-  -- TODO
+  -- Configure 'select' options using enumeration.
+  --
+  -- TODO: Add an example.
   --
-  addNote :: (Monad m) => FormNote l -> FieldT l a m ()
-  addNote note = FieldT do
-    modify \(view, value) ->
-      ( view { viewNotes = viewNotes view <> [note] }
-      , value
-      )
+  optionsFromEnum :: (Monad m, Param v, Selectable v, Bounded v, Enum v)
+                  => (v -> l) -> ControlT 'SelectFieldTag l o v m ()
+  optionsFromEnum label = optionsFromValues label [minBound..maxBound]
 
 
   -- |
   -- TODO
   --
-  addAttribute :: (Monad m, Typeable v) => Text -> v -> FieldT l a m ()
-  addAttribute name attr = FieldT do
-    modify \(view, value) ->
-      ( view { viewAttrs = viewAttrs view <> [(name, toDyn attr)] }
-      , value
-      )
+  validate :: (Monad m) => (Maybe v -> m [Note l]) -> ControlT t l o v m ()
+  validate fn = ControlT do
+    modify \s@ControlState{..} ->
+      s { csValidators = csValidators <> [fn] }
 
 
   -- |
   -- TODO
   --
-  processValue :: (Monad m) => (Maybe a -> Maybe a) -> FieldT l a m ()
-  processValue process = FieldT do
-    modify \(view, value) -> (view, process value)
+  check :: (Monad m) => (Maybe v -> [Note l]) -> ControlT t l o v m ()
+  check fn = validate (return . fn)
 
 
   -- |
   -- TODO
   --
-  whenChecking :: (Monad m) => FieldT l a m b -> FieldT l a m ()
-  whenChecking checkField = do
-    check <- fieldShouldCheck
-
-    if check
-       then checkField >> return ()
-       else return ()
+  note :: (Monad m) => Severity -> l -> ControlT t l o v m ()
+  note sev msg = validate (\_ -> return [Note sev msg])
 
 
   -- |
   -- TODO
   --
-  fieldShouldCheck :: (Monad m) => FieldT l a m Bool
-  fieldShouldCheck = FieldT ask
+  hint :: (Monad m, Typeable h) => h -> ControlT t l o v m ()
+  hint x = ControlT do
+    modify \s@ControlState{..} ->
+      s { csHints = csHints <> [toDyn x] }
 
 
-  -- |
-  -- TODO
-  --
-  fieldValue :: (Monad m) => FieldT l a m (Maybe a)
-  fieldValue = FieldT (snd <$> get)
+  -- Debugging ---------------------------------------------------------------
 
 
-  -- |
-  -- Determine whether the view has any (possibly nested) errors.
-  --
-  hasErrors :: View l -> Bool
-  hasErrors FormFields{..} = any isErrorNote viewNotes || any hasErrors viewFields
-  hasErrors view           = any isErrorNote (viewNotes view)
+  dumpForm :: (MonadIO m, Show l, Show o) => FormT l o m ()
+  dumpForm = FormT do
+    env <- ask
+
+    liftIO do
+      putStr "Form Dump:\n  "
+      putStrLn (show env)
+
+    return $ Just ()
 
 
-  -- Form Internals ---------------------------------------------------------
+  dumpControl :: (MonadIO m, Show l, Show o, Show v) => ControlT t l o v m ()
+  dumpControl = ControlT do
+    state <- get
 
+    liftIO do
+      putStr "Control Dump:\n  "
+      putStrLn (show state)
 
-  runFieldT :: (Monad m) => FieldT l a m b -> Maybe a -> View l
-             -> FormT l m (View l, Maybe a)
-  runFieldT field value view = do
-    Env{envCheck} <- ask
-    lift $ execStateT (runReaderT (unFieldT field) envCheck) (view, value)
 
+  -- Internal ----------------------------------------------------------------
 
-  isErrorNote :: FormNote l -> Bool
-  isErrorNote (FormNote NoteError _) = True
-  isErrorNote _else                  = False
 
+  getParamOrig :: (Monad m, Param v)
+               => Text -> Maybe v -> ReaderT (Env o) m (Maybe v, Text)
+  getParamOrig name orig = do
+    Env{..} <- ask
 
-  formParamMaybe :: (Monad m, Param a) => Text -> FormT l m (Maybe a)
-  formParamMaybe name = do
-    Env{envParams} <- ask
-    return $ fromParam =<< lookup name envParams
+    case envParams of
+      Nothing -> return (orig, maybe "" toParam orig)
+      Just ps -> let param = strip <$> lookup (envPrefix <> "." <> name) ps
+                  in return (fromParam =<< param, fromMaybe "" param)
 
 
-  formFileMaybe :: (Monad m) => Text -> FormT l m (Maybe FilePath)
-  formFileMaybe name = do
-    Env{envFiles} <- ask
-    return $ lookup name envFiles
+  getSelectParams :: (Monad m, Param v, Selectable v)
+                  => Text -> ReaderT (Env o) m (Maybe v)
+  getSelectParams name = do
+    Env{..} <- ask
+    let name' = envPrefix <> "." <> name
+    return $ selectValues . lookupList name' =<< envParams
 
 
-  formParams :: (Monad m, Param a) => Text -> FormT l m [a]
-  formParams name = do
-    Env{envParams} <- ask
-    let match = (name ==) . fst
-        conv  = fromParam . snd
-     in return $ mapMaybe conv $ filter match $ envParams
+  lookupList :: (Eq a) => a -> [(a, b)] -> [b]
+  lookupList name = map snd . filter ((name ==) . fst)
+
+
+  buildControl :: (Monad m, FromFormMessage l)
+               => Env o
+               -> ControlState l v m
+               -> m (Control l)
+  buildControl Env{..} ControlState{..} = do
+    vres <- case envValidate of
+              True  -> sequence $ map ($ csValue) (required : csValidators)
+              False -> return []
+
+    return Control { ctrlName       = envPrefix <> "." <> csName
+                   , ctrlField      = csField
+                   , ctrlNotes      = mconcat vres
+                   , ctrlHints      = csHints
+                   }
 
 
-  makeName :: (Monad m) => Text -> FormT l m Text
-  makeName name = do
-    Env{envPrefix} <- ask
-    return $ mconcat $ intersperse "." $ reverse (name : envPrefix)
+  required :: (Monad m, FromFormMessage l) => Maybe v -> m [Note l]
+  required Nothing = return [Note Danger $ fromFormMessage FormMsgFieldRequired]
+  required _else   = return []
 
 
 -- vim:set ft=haskell sw=2 ts=2 et:
diff --git a/lib/Hikaru/Localize.hs b/lib/Hikaru/Localize.hs
index 8b6ba09d21172f2b62dc325c91fb964aeb0a6a0e..cc529266426c1e1004fd6e4a0faf3e1c41bc944f 100644
--- a/lib/Hikaru/Localize.hs
+++ b/lib/Hikaru/Localize.hs
@@ -16,33 +16,32 @@ First, you need to create a message catalog:
 data SampleMessages
   = MsgSuccess
   | MsgFailure
+  deriving (Show)
 
 -- Default HTML rendering of the messages.
 instance 'ToHtml' SampleMessages where
   'toHtmlRaw' = 'toHtml'
-  'toHtml' MsgSuccess = \"Success\"
-  'toHtml' MsgFailure = \"Failure\"
 
 -- Language-specific rendering of those messages.
-instance Localized SampleMessages where
-  -- Czech variants
-  localize \"cs\" MsgSuccess = 'Just' \"Úspěch\"
-  localize \"cs\" MsgFailure = 'Just' \"Selhání\"
+instance Localizable SampleMessages where
+  -- English variants
+  'localize' \"en\" MsgSuccess = 'Just' \"Success\"
+  'localize' \"en\" MsgFailure = 'Just' \"Failure\"
 
-  -- English is the default
-  localize \"en\" msg        = 'Just' ('toHtml' msg)
+  -- Czech variants
+  'localize' \"cs\" MsgSuccess = 'Just' \"Úspěch\"
+  'localize' \"cs\" MsgFailure = 'Just' \"Selhání\"
 
   -- Otherwise try the next locale
-  localize _locale _msg    = 'Nothing'
+  'localize' _locale _msg = 'Nothing'
 @
 
 Next, create a preferred language list for every action:
 
 @
 'Hikaru.Dispatch.dispatch' runAction $ do
-  'Hikaru.Dispatch.wrapActions' ('selectLanguages' \"lang\" \"lang\" >>)
-
-  'Hikaru.Dispatch.route' ...
+  'Hikaru.Dispatch.wrapAction' ('selectLanguages' \"lang\" \"lang\" >>) $ do
+    'Hikaru.Dispatch.route' ...
 @
 
 Finally, you can use your catalog when rendering pages:
@@ -59,7 +58,8 @@ getSampleR flag = do
 
 module Hikaru.Localize
   ( Locale
-  , Localized(..)
+  , Localizable(..)
+  , lc
   , lc_
   , selectLanguages
   )
@@ -84,35 +84,65 @@ where
   -- |
   -- Any message that can be rendered localized.
   --
-  class (ToHtml a) => Localized a where
+  class (Show l) => Localizable l where
     -- |
     -- Try to localize the message using given locale.
     -- Return 'Nothing' if the locale is not supported.
     --
-    localize :: (Monad m) => Locale -> a -> Maybe (HtmlT m ())
-    localize _lc = const Nothing
+    localize :: Locale -> l -> Maybe Text
+    localize _lang _msg = Nothing
     {-# INLINE localize #-}
 
+    -- |
+    -- Same as 'localize', but for HTML.
+    -- Defaults to using the plain 'localize'.
+    --
+    localizeHtml :: (Monad m) => Locale -> l -> Maybe (HtmlT m ())
+    localizeHtml lang = fmap toHtml . localize lang
+    {-# INLINE localizeHtml #-}
+
 
   -- |
   -- Instance to make 'Text' usable for interoperability and
   -- gradual localization.
   --
-  instance Localized Text where
-    localize _lc = Just . toHtml
+  instance Localizable Text where
+    localize _lc = Just
+    {-# INLINE localize #-}
+
+
+  -- |
+  -- Instance to make 'Maybe' values simpler to render localized.
+  --
+  instance Localizable l => Localizable (Maybe l) where
+    localize lang (Just msg) = localize lang msg
+    localize _ Nothing       = Just ""
     {-# INLINE localize #-}
 
 
+  -- |
+  -- Localize given message to the language indicated by the
+  -- 'getLanguages' of the current action. Uses 'localize' internally.
+  --
+  lc :: (MonadAction m, Localizable l) => l -> m Text
+  lc msg = do
+    langs <- getLanguages
+
+    case mapMaybe (flip localize msg) langs of
+      []  -> return $ cs $ show msg
+      x:_ -> return x
+
+
   -- |
   -- Localize given message to the language indicated by the
   -- 'getLanguages' of the current action.
   --
-  lc_ :: (MonadAction m, Localized a) => a -> HtmlT m ()
+  lc_ :: (MonadAction m, Localizable l) => l -> HtmlT m ()
   lc_ msg = do
     langs <- getLanguages
 
-    case mapMaybe (flip localize msg) langs of
-      []  -> toHtml msg
+    case mapMaybe (flip localizeHtml msg) langs of
+      []  -> toHtml $ show msg
       x:_ -> x
 
 
diff --git a/lib/Hikaru/Types.hs b/lib/Hikaru/Types.hs
index 4d37016584d43ab2be60d4f4947019fc0ab3c225..115e11c7a9a285946924f4561f1a869666874e6f 100644
--- a/lib/Hikaru/Types.hs
+++ b/lib/Hikaru/Types.hs
@@ -22,8 +22,8 @@ where
   import Data.ByteString (ByteString)
   import Data.String.Conversions
   import Data.Text (Text, pack, unpack)
-  import Network.HTTP.Types.Status
   import Network.HTTP.Types.Header
+  import Network.HTTP.Types.Status
   import Network.Wai
 
   import qualified Data.ByteString.Lazy
@@ -181,21 +181,24 @@ where
     {-# INLINE toParam #-}
 
   instance Param Text where
-    fromParam = Just . id
+    fromParam "" = Nothing
+    fromParam sp = Just sp
     {-# INLINE fromParam #-}
 
     toParam = id
     {-# INLINE toParam #-}
 
   instance Param Data.Text.Lazy.Text where
-    fromParam = Just . Data.Text.Lazy.fromStrict
+    fromParam "" = Nothing
+    fromParam sp = Just $ Data.Text.Lazy.fromStrict sp
     {-# INLINE fromParam #-}
 
     toParam = Data.Text.Lazy.toStrict
     {-# INLINE toParam #-}
 
   instance Param Data.ByteString.ByteString where
-    fromParam = Just . Data.Text.Encoding.encodeUtf8
+    fromParam "" = Nothing
+    fromParam sp = Just $ Data.Text.Encoding.encodeUtf8 sp
     {-# INLINE fromParam #-}
 
     toParam = Data.Text.Encoding.decodeUtf8With
@@ -203,8 +206,9 @@ where
     {-# INLINE toParam #-}
 
   instance Param Data.ByteString.Lazy.ByteString where
-    fromParam = Just . Data.ByteString.Lazy.fromStrict
-                     . Data.Text.Encoding.encodeUtf8
+    fromParam "" = Nothing
+    fromParam sp = Just $ Data.ByteString.Lazy.fromStrict
+                        $ Data.Text.Encoding.encodeUtf8 sp
     {-# INLINE fromParam #-}
 
     toParam = Data.Text.Encoding.decodeUtf8With
diff --git a/package.yaml b/package.yaml
index b78b5ab9956088cb8553cc1b420e856d997408d3..6e2a8b8620c4270dbde003b48741d383cfd3b2a0 100644
--- a/package.yaml
+++ b/package.yaml
@@ -26,6 +26,7 @@ default-extensions:
   - BlockArguments
   - DataKinds
   - DefaultSignatures
+  - DeriveGeneric
   - FlexibleInstances
   - GADTs
   - GeneralizedNewtypeDeriving
@@ -78,4 +79,17 @@ library:
     - Hikaru.Route
     - Hikaru.Types
 
+tests:
+  spec:
+    main: Spec.hs
+    source-dirs: test
+    ghc-options: -threaded -rtsopts -with-rtsopts=-N
+    cpp-options: -DTEST
+
+    dependencies:
+      - hikaru
+      - hspec
+
+    build-tools: hspec-discover
+
 # EOF
diff --git a/test/Hikaru/Demo.hs b/test/Hikaru/Demo.hs
new file mode 100644
index 0000000000000000000000000000000000000000..f4b68a6bdd0b922af9c10bca5d4c99a57474dda2
--- /dev/null
+++ b/test/Hikaru/Demo.hs
@@ -0,0 +1,357 @@
+{-|
+Module      :  Hikaru.Demo
+Copyright   :  Jan Hamal Dvořák
+License     :  AGPL-3
+
+Maintainer  :  mordae@anilinux.org
+Stability   :  unstable
+Portability :  non-portable (ghc)
+-}
+
+module Hikaru.Demo
+  ( makeDemo
+  )
+where
+  import BasePrelude hiding (for_, Option, Control)
+
+  import Control.Monad.Reader
+  import Data.Aeson ()
+  import Data.Text (Text)
+  import Hikaru
+  import Lucid
+  import Network.HTTP.Types.Header
+  import Network.HTTP.Types.Status
+  import Network.Wai
+
+
+  -- Action ------------------------------------------------------------------
+
+
+  -- |
+  -- Our custom action monad allows us to inspect request,
+  -- build response and consult the model at the same time.
+  --
+  newtype Action a
+    = Action
+      { unAction         :: ReaderT DemoEnv IO a
+      }
+    deriving (Functor, Applicative, Monad, MonadIO)
+
+  instance MonadAction Action where
+    getActionEnv = Action (demoActionEnv <$> ask)
+
+  instance MonadModel Action where
+    getModelEnv = Action (demoModelEnv <$> ask)
+
+
+  data DemoEnv
+    = DemoEnv
+      { demoActionEnv  :: ActionEnv
+      , demoModelEnv   :: ModelEnv
+      }
+
+
+  -- Model -------------------------------------------------------------------
+
+
+  class (MonadIO m) => MonadModel m where
+    getModelEnv :: m ModelEnv
+
+
+  data ModelEnv
+    = ModelEnv
+      { modelCounter   :: MVar Word
+      , modelCases     :: MVar [Case]
+      }
+
+
+  makeModelEnv :: Word -> IO ModelEnv
+  makeModelEnv n = ModelEnv <$> newMVar n <*> newMVar []
+
+
+  countVisitor :: (MonadModel m) => m Word
+  countVisitor = do
+    counter <- modelCounter <$> getModelEnv
+    liftIO do
+      modifyMVar_ counter (return . succ)
+      readMVar counter
+
+
+  addCase :: (MonadModel m) => AddCase -> m Case
+  addCase AddCase{..} = do
+    nextId  <- liftIO . readMVar . modelCounter =<< getModelEnv
+
+    let case' = Case { caseId     = nextId
+                     , caseName   = acName
+                     , caseRecNo  = fromMaybe acName acRecNo
+                     , caseMode   = acMode
+                     , caseActive = acActive
+                     }
+
+    cases <- modelCases <$> getModelEnv
+
+    liftIO do
+      modifyMVar_ cases (return . (<> [case']))
+      return case'
+
+
+  -- Dispatching -------------------------------------------------------------
+
+
+  makeDemo :: IO Application
+  makeDemo = do
+    model <- makeModelEnv 0
+    return $ makeApplication model
+
+
+  runAction :: ModelEnv -> Action () -> Application
+  runAction me act = do
+    respond $ \ae -> do
+      runReaderT (unAction act) (DemoEnv ae me)
+
+
+  makeApplication :: ModelEnv -> Application
+  makeApplication me = do
+    dispatch (runAction me) do
+      -- Register nicer 404 error handler.
+      handler NotFound handleNotFound
+
+      -- Negotiate content for the root page.
+      route $ getRootHtmlR <$ get <* offerHTML
+      route $ getRootTextR <$ get <* offerText
+
+      -- Disable caching for all the following endpoints.
+      wrapAction (defaultHeader hCacheControl "no-store" >>) do
+        -- Present a simple greeting page.
+        route $ getHelloR <$ get <* seg "hello" <*> arg
+                          <* offerText
+
+        -- Present an echoing JSON API.
+        route $ postEchoR <$ post <* seg "api" <* seg "echo"
+                          <* offerJSON <* acceptJSON
+
+        -- Case handling.
+        route $ postCaseR <$ post <* seg "case" <* seg "" <* acceptForm
+        route $ getCasesR <$ get  <* seg "case" <* seg "" <* offerJSON
+
+
+  -- Handlers ----------------------------------------------------------------
+
+
+  getRootHtmlR :: Action ()
+  getRootHtmlR = do
+    -- Update the counter.
+    n <- countVisitor
+
+    -- Present fancy HTML result.
+    sendHTML do
+      h1_ "Welcome!"
+      p_ $ "You are " >> toHtml (show n) >> ". visitor!"
+
+
+  getRootTextR :: Action ()
+  getRootTextR = do
+    -- Update the counter.
+    n <- countVisitor
+
+    -- Present a plain textual result.
+    sendString $ unlines [ "Welcome!"
+                         , "You are " <> show n <> ". visitor!"
+                         ]
+
+
+  postEchoR :: Action ()
+  postEchoR = sendJSON =<< getJSON
+
+
+  getHelloR :: Text -> Action ()
+  getHelloR name = sendText $ "Hello, " <> name <> "!"
+
+
+  handleNotFound :: RequestError -> Text -> Action ()
+  handleNotFound _exn msg = do
+    setStatus status404
+    sendHTML do
+      h1_ "404 Not Found"
+      p_ (toHtml msg)
+
+
+  postCaseR :: Action ()
+  postCaseR = do
+    (res, view) <- postForm "addCase" addCaseForm
+
+    case res of
+      Nothing -> do
+        setStatus status400
+        sendHTML do
+          simpleForm_ view
+
+      Just ac -> do
+        _case <- addCase ac
+        redirect "/case/"
+
+
+  getCasesR :: Action ()
+  getCasesR = do
+    cases <- liftIO . readMVar . modelCases =<< getModelEnv
+
+    sendHTML do
+      h1_ "Cases"
+
+      form_ [method_ "POST"] do
+        view <- newForm "addCase" Nothing addCaseForm
+        simpleForm_ view
+        button_ [type_ "submit"] "Submit"
+
+      table_ do
+        tr_ do
+          th_ "Id"
+          th_ "Name"
+          th_ "RecNo"
+          th_ "Mode"
+          th_ "Active"
+
+        for cases \Case{..} -> do
+          tr_ do
+            td_ $ toHtml $ show caseId
+            td_ $ toHtml $ caseName
+            td_ $ toHtml $ caseRecNo
+            td_ $ toHtml $ show caseMode
+            td_ $ toHtml $ show caseActive
+
+
+  -- Forms -------------------------------------------------------------------
+
+
+  simpleForm_ :: (MonadAction m, Localizable l) => View l -> HtmlT m ()
+  simpleForm_ View{..} = do
+    forM_ viewControls \ctrl@Control{..} -> do
+      viewControl_ ctrl
+      forM_ ctrlNotes \Note{..} -> do
+        p_ do
+          lc_ noteMessage
+
+    forM_ viewElements \Element{..} -> do
+      div_ do
+        div_ do
+          case elemControls of
+            Control{..}:_ -> label_ [for_ ctrlName] $ lc_ elemLabel
+            _otherwise    -> label_ $ lc_ elemLabel
+
+        div_ do
+          forM_ elemControls \ctrl@Control{..} -> do
+            viewControl_ ctrl
+
+          forM_ elemControls \Control{..} -> do
+            forM_ ctrlNotes \Note{..} -> do
+              p_ do
+                lc_ noteMessage
+
+
+  viewControl_ :: (Localizable l, MonadAction m) => Control l -> HtmlT m ()
+  viewControl_ Control{..} = do
+    case ctrlField of
+      InputField{..} -> do
+        ph <- lc fieldPlacehold
+        input_ [ type_ fieldType
+               , name_ ctrlName
+               , placeholder_ ph
+               , value_ fieldValue
+               ]
+
+      SelectField{..} -> do
+        select_ [name_ ctrlName] do
+          mapM_ viewOption_ fieldOptions
+
+
+  viewOption_ :: (MonadAction m, Localizable l) => Option l -> HtmlT m ()
+  viewOption_ Option{..} = do
+    case optionSelected of
+      True -> do
+        option_ [selected_ "selected", value_ optionValue] do
+          lc_ optionLabel
+
+      False -> do
+        option_ [value_ optionValue] do
+          lc_ optionLabel
+
+
+  data Case
+    = Case
+      { caseId         :: Word
+      , caseName       :: Text
+      , caseRecNo      :: Text
+      , caseMode       :: AccessMode
+      , caseActive     :: Bool
+      }
+    deriving (Show, Generic)
+
+
+  data AddCase
+    = AddCase
+      { acName         :: Text
+      , acRecNo        :: Maybe Text
+      , acMode         :: AccessMode
+      , acActive       :: Bool
+      }
+
+  data AccessMode
+    = ModePublic
+    | ModePrivate
+    deriving (Show, Eq)
+
+  instance Param AccessMode where
+    toParam ModePublic  = "public"
+    toParam ModePrivate = "private"
+
+    fromParam "public"  = Just ModePublic
+    fromParam "private" = Just ModePrivate
+    fromParam _else     = Nothing
+
+
+  data RenderHint
+    = RenderCollapsed
+    | RenderExpanded
+    deriving (Eq, Show)
+
+
+  data Messages
+    = MsgCaseName
+    | MsgCaseMode
+    | MsgCaseEnabled
+    | MsgForm FormMessage
+    deriving (Show)
+
+  instance Localizable Messages where
+    localize "en" MsgCaseName    = Just "Name"
+    localize "en" MsgCaseMode    = Just "Mode"
+    localize "en" MsgCaseEnabled = Just "Enabled"
+
+    localize lang (MsgForm msg)  = localize lang msg
+    localize _lang _msg          = Nothing
+
+  instance FromFormMessage Messages where
+    fromFormMessage = MsgForm
+
+
+  addCaseForm :: (MonadAction m) => Form Messages m AddCase
+  addCaseForm = do
+    (\(x1, x2) x3 x4 -> AddCase x1 x2 x3 x4)
+      <$> element MsgCaseName do
+            (,)
+              <$> input "name" acName do
+                    return ()
+
+              <*> input "recno" acRecNo do
+                    return ()
+
+      <*> element MsgCaseMode do
+            select "mode" acMode do
+              return ()
+
+      <*> element MsgCaseEnabled do
+            select "active" acActive do
+              hint RenderExpanded
+
+
+-- vim:set ft=haskell sw=2 ts=2 et:
diff --git a/test/Hikaru/DemoSpec.hs b/test/Hikaru/DemoSpec.hs
new file mode 100644
index 0000000000000000000000000000000000000000..35f0057bd4558e4627807e832d60785f4653bfef
--- /dev/null
+++ b/test/Hikaru/DemoSpec.hs
@@ -0,0 +1,76 @@
+{-|
+Module      :  Hikaru.DemoSpec
+Copyright   :  Jan Hamal Dvořák
+License     :  AGPL-3
+
+Maintainer  :  mordae@anilinux.org
+Stability   :  unstable
+Portability :  non-portable (ghc)
+
+Smoke tests coverting a simple demo site.
+-}
+
+module Hikaru.DemoSpec
+  ( spec
+  )
+where
+  import BasePrelude
+
+  import Hikaru.Demo
+  import Hikaru.Test
+
+
+  spec :: Spec
+  spec = do
+    describe "GET /" do
+      context "by default" do
+        it "responds with 200 and text/html" do
+          runDemo do
+            resp <- get "/" []
+            assertStatus 200 resp
+            assertHeader hContentType "text/html; charset=utf8" resp
+            assertBodyContains "<h1>Welcome" resp
+            assertBodyContains " 1. " resp
+
+      context "when asked for text/plain" do
+        it "responds with 200 and text/plain" do
+          runDemo do
+            resp <- get "/" [(hAccept, "text/plain")]
+            assertStatus 200 resp
+            assertHeader hContentType "text/plain; charset=utf8" resp
+            assertBodyContains "Welcome" resp
+            assertBodyContains " 2. " resp
+
+    describe "GET /404" do
+      it "responds with 404" do
+        runDemo do
+          resp <- get "/404" []
+          assertStatus 404 resp
+          assertBodyContains "404" resp
+
+    describe "GET /hello/<arg>" do
+      it "greets caller" do
+        runDemo do
+          resp <- get "/hello/Tester" []
+          assertStatus 200 resp
+          assertHeader hContentType "text/plain; charset=utf8" resp
+          assertHeader hCacheControl "no-store" resp
+          assertBody "Hello, Tester!" resp
+
+    describe "POST /api/echo" do
+      it "echoes JSON payload" do
+        runDemo do
+          resp <- post "/api/echo" [(hContentType, "application/json")] "[1, 2]"
+          assertStatus 200 resp
+          assertBody "[1,2]" resp
+
+
+  demo :: Application
+  demo = unsafePerformIO makeDemo
+
+
+  runDemo :: Session a -> IO a
+  runDemo s = runSession s demo
+
+
+-- vim:set ft=haskell sw=2 ts=2 et:
diff --git a/test/Hikaru/FormSpec.hs b/test/Hikaru/FormSpec.hs
new file mode 100644
index 0000000000000000000000000000000000000000..a845d7c1e73727053c9e8350123bf6441dca8306
--- /dev/null
+++ b/test/Hikaru/FormSpec.hs
@@ -0,0 +1,31 @@
+{-|
+Module      :  Hikaru.FormSpec
+Copyright   :  Jan Hamal Dvořák
+License     :  AGPL-3
+
+Maintainer  :  mordae@anilinux.org
+Stability   :  unstable
+Portability :  non-portable (ghc)
+-}
+
+module Hikaru.FormSpec
+  ( spec
+  )
+where
+  import BasePrelude
+
+  import Hikaru ()
+  import Hikaru.Test
+
+
+  -- Spec --------------------------------------------------------------------
+
+
+  spec :: Spec
+  spec = do
+    describe "form" do
+      it "has tests written" do
+        False
+
+
+-- vim:set ft=haskell sw=2 ts=2 et:
diff --git a/test/Hikaru/Test.hs b/test/Hikaru/Test.hs
new file mode 100644
index 0000000000000000000000000000000000000000..5941094b574eb80eb61632bc8878d540cf9130cb
--- /dev/null
+++ b/test/Hikaru/Test.hs
@@ -0,0 +1,55 @@
+{-|
+Module      :  Hikaru.Test
+Copyright   :  Jan Hamal Dvořák
+License     :  AGPL-3
+
+Maintainer  :  mordae@anilinux.org
+Stability   :  unstable
+Portability :  non-portable (ghc)
+
+Common testing definitions.
+-}
+
+module Hikaru.Test
+  ( get
+  , post
+
+  , module Network.HTTP.Types
+  , module Network.Wai
+  , module Network.Wai.Internal
+  , module Network.Wai.Test
+  , module Test.Hspec
+  )
+where
+  import BasePrelude
+
+  import qualified Data.ByteString.Lazy as Lazy
+
+  import Data.ByteString (ByteString)
+  import Network.HTTP.Types
+  import Network.Wai
+  import Network.Wai.Internal
+  import Network.Wai.Test
+  import Test.Hspec
+
+
+  get :: ByteString -> RequestHeaders -> Session SResponse
+  get path headers =
+    let req = setPath defaultRequest path
+     in request $ req { requestMethod = methodGet
+                      , requestHeaders = headers
+                      }
+
+
+  post :: ByteString -> RequestHeaders -> ByteString -> Session SResponse
+  post path headers body = srequest sreq
+    where
+      req = setPath defaultRequest path
+      sreq = SRequest { simpleRequestBody = Lazy.fromStrict body
+                      , simpleRequest     = req { requestMethod  = methodPost
+                                                , requestHeaders = headers
+                                                }
+                      }
+
+
+-- vim:set ft=haskell sw=2 ts=2 et:
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
index 0000000000000000000000000000000000000000..8f12fe38bace2b23f8ef24256b1f716ee844274a
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,14 @@
+{-|
+Module      :  Spec
+Copyright   :  Jan Hamal Dvořák
+License     :  AGPL-3
+
+Maintainer  :  mordae@anilinux.org
+Stability   :  unstable
+Portability :  non-portable (ghc)
+-}
+
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
+
+
+-- vim:set ft=haskell sw=2 ts=2 et: