diff --git a/apiary.cabal b/apiary.cabal
--- a/apiary.cabal
+++ b/apiary.cabal
@@ -1,5 +1,5 @@
 name:                apiary
-version:             0.12.7
+version:             0.12.8
 synopsis:            Simple and type safe web framework that can be automatically generate API documentation.
 description:
   Simple and type safe web framework that can be automatically generate API documentation.
@@ -35,9 +35,9 @@
   .
     * type safe route filter.
   .
-    * auto generate API documentation(example: <https://github.com/philopon/apiary/blob/v0.12.7/examples/api.hs>, <https://rawgit.com/philopon/apiary/v0.12.7/examples/api.html>).
+    * auto generate API documentation(example: <https://github.com/philopon/apiary/blob/v0.12.8/examples/api.hs>, <https://rawgit.com/philopon/apiary/v0.12.8/examples/api.html>).
   .
-  more examples: <https://github.com/philopon/apiary/blob/v0.12.7/examples/>
+  more examples: <https://github.com/philopon/apiary/blob/v0.12.8/examples/>
 
 license:             MIT
 license-file:        LICENSE
@@ -49,7 +49,9 @@
 category:            Web
 build-type:          Simple
 stability:           experimental
--- extra-source-files:  
+extra-source-files:  static/api-documentation.min.js
+                   , static/jquery.cookie-1.4.1.min.js
+                   , static/api-documentation.min.css
 cabal-version:       >=1.10
 
 flag wai3
@@ -99,6 +101,7 @@
                      , mime-types           >=0.1   && <0.2
                      , exceptions           >=0.6   && <0.7
                      , blaze-html           >=0.7   && <0.8
+                     , blaze-markup         >=0.6   && <0.7
 
   if impl(ghc < 7.8)
     build-depends:     tagged               >=0.7   && <0.8
diff --git a/src/Control/Monad/Apiary/Action.hs b/src/Control/Monad/Apiary/Action.hs
--- a/src/Control/Monad/Apiary/Action.hs
+++ b/src/Control/Monad/Apiary/Action.hs
@@ -4,6 +4,7 @@
     ( ActionT
     , ApiaryConfig(..)
     , defaultDocumentationAction
+    , DefaultDocumentConfig(..)
     -- * actions
     , stop, stopWith
 
@@ -45,6 +46,7 @@
 import Control.Monad.Apiary.Action.Internal
 import Data.Default.Class
 import Data.Apiary.Param
+import Data.Apiary.Document
 import Network.HTTP.Types.Status hiding (mkStatus)
 
 #ifdef WAI3
diff --git a/src/Control/Monad/Apiary/Action/Internal.hs b/src/Control/Monad/Apiary/Action/Internal.hs
--- a/src/Control/Monad/Apiary/Action/Internal.hs
+++ b/src/Control/Monad/Apiary/Action/Internal.hs
@@ -30,10 +30,10 @@
 import Data.Apiary.Document
 import Data.Default.Class
 import Blaze.ByteString.Builder
-import Text.Blaze.Html
 import Text.Blaze.Html.Renderer.Utf8
 import qualified Data.ByteString as S
 import qualified Data.ByteString.Lazy as L
+import qualified Data.ByteString.Lazy.Char8 as LC
 import qualified Data.Text as T
 
 #ifndef WAI3
@@ -46,19 +46,21 @@
       -- | used unless call 'status' function.
     , defaultStatus       :: Status
       -- | initial headers.
-    , defaultHeader       :: ResponseHeaders
+    , defaultHeaders      :: ResponseHeaders
+    , failStatus          :: Status
+    , failHeaders         :: ResponseHeaders
       -- | used by 'Control.Monad.Apiary.Filter.root' filter.
     , rootPattern         :: [S.ByteString]
     , mimeType            :: FilePath -> S.ByteString
-    , documentationAction :: Documents -> ActionT IO ()
+    , documentationAction :: Maybe (Documents -> ActionT IO ())
     }
 
-defaultDocumentationAction :: Monad m => S.ByteString -> T.Text -> Maybe Html -> Documents -> ActionT m ()
-defaultDocumentationAction r t md d = do
+defaultDocumentationAction :: Monad m => S.ByteString -> DefaultDocumentConfig -> Documents -> ActionT m ()
+defaultDocumentationAction r conf d = do
     p <- rawPathInfo <$> getRequest
     guard $ p == r
     contentType "text/html"
-    builder . renderHtmlBuilder $ defaultDocumentToHtml t md d
+    builder . renderHtmlBuilder $ defaultDocumentToHtml conf d
 
 defNotFound :: Application
 #ifdef WAI3
@@ -71,10 +73,13 @@
     def = ApiaryConfig 
         { notFound            = defNotFound
         , defaultStatus       = ok200
-        , defaultHeader       = []
+        , defaultHeaders      = []
+        , failStatus          = internalServerError500
+        , failHeaders         = []
         , rootPattern         = ["", "/", "/index.html", "/index.htm"]
         , mimeType            = defaultMimeLookup . T.pack
-        , documentationAction = defaultDocumentationAction "/api/documentation" "API documentation" Nothing
+        , documentationAction = Just $ defaultDocumentationAction
+            "/api/documentation" def
         }
 
 convFile :: (S.ByteString, P.FileInfo L.ByteString) -> File
@@ -90,9 +95,9 @@
 
 initialState :: ApiaryConfig -> Request -> ActionState
 initialState conf req = ActionState
-    { actionResponse = responseLBS (defaultStatus conf) (defaultHeader conf) ""
-    , actionStatus   = defaultStatus conf
-    , actionHeaders  = defaultHeader conf
+    { actionResponse = responseLBS (defaultStatus conf) (defaultHeaders conf) ""
+    , actionStatus   = defaultStatus  conf
+    , actionHeaders  = defaultHeaders conf
     , actionReqBody  = Nothing
     , actionPathInfo = pathInfo req
     }
@@ -130,7 +135,8 @@
     m >>= k  = ActionT $ \conf req st cont ->
         unActionT m conf req st $ \a st' ->
         st' `seq` unActionT (k a) conf req st' cont
-    fail _ = ActionT $ \_ _ _ _ -> return Pass
+    fail s = ActionT $ \c _ _ _ -> return $
+        Stop (responseLBS (failStatus c) (failHeaders c) $ LC.pack s)
 
 instance MonadIO m => MonadIO (ActionT m) where
     liftIO m = ActionT $ \_ _ st cont ->
diff --git a/src/Control/Monad/Apiary/Internal.hs b/src/Control/Monad/Apiary/Internal.hs
--- a/src/Control/Monad/Apiary/Internal.hs
+++ b/src/Control/Monad/Apiary/Internal.hs
@@ -108,7 +108,8 @@
             -> ApiaryT '[] n m a -> m (Application, Documents)
 runApiaryT' run conf m = unApiaryT m (initialReader conf) (\_ w -> return w) >>= \wtr -> do
     let doc = docsToDocuments $ writerDoc wtr
-        app = execActionT conf $ hoistActionT run (writerHandler wtr) `mplus` documentationAction conf doc
+        da  = maybe mzero (\f -> f doc) $ documentationAction conf
+        app = execActionT conf $ hoistActionT run (writerHandler wtr) `mplus` da
     return (app, doc)
 
 runApiaryT :: (Monad n, Monad m) => (forall b. n b -> IO b) -> ApiaryConfig
diff --git a/src/Data/Apiary/Document.hs b/src/Data/Apiary/Document.hs
--- a/src/Data/Apiary/Document.hs
+++ b/src/Data/Apiary/Document.hs
@@ -1,10 +1,12 @@
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE LambdaCase #-}
 {-# LANGUAGE TupleSections #-}
-{-# LANGUAGE NoMonomorphismRestriction #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE RecordWildCards #-}
 
 module Data.Apiary.Document where
 
+import Language.Haskell.TH
 import Control.Applicative
 import qualified Data.Text as T
 import qualified Data.Text.Encoding as T
@@ -14,9 +16,11 @@
 import Data.Apiary.Param
 import qualified Network.HTTP.Types as HT
 import Text.Blaze.Html
+import Text.Blaze.Internal(attribute)
 import qualified Text.Blaze.Html5 as H
 import qualified Text.Blaze.Html5.Attributes as A
 import Data.Monoid
+import Data.Default.Class
 import Data.List
 import Data.Function
 
@@ -113,7 +117,7 @@
     loop _ r p End =
         (r, if null p
             then mempty
-            else H.table ! A.class_ "table table-condensed" $
+            else H.table ! A.class_ "table table-condensed route-parameters" $
                  H.tr (mconcat 
                     [ H.th ! A.class_ "col-sm-1 com-md-1" $ "#"
                     , H.th ! A.class_ "col-sm-1 com-md-1" $ "type"
@@ -122,27 +126,29 @@
         )
 
 
-defaultDocumentToHtml :: T.Text -> Maybe Html -> Documents -> Html
-defaultDocumentToHtml title desc docs = H.docTypeHtml $ H.head headH <> H.body body <> footer
+data DefaultDocumentConfig = DefaultDocumentConfig
+    { documentTitle       :: T.Text
+    , documentDescription :: Maybe Html
+    }
+
+instance Default DefaultDocumentConfig where
+    def = DefaultDocumentConfig "API documentation" Nothing
+
+defaultDocumentToHtml :: DefaultDocumentConfig -> Documents -> Html
+defaultDocumentToHtml DefaultDocumentConfig{..} docs =
+    H.docTypeHtml $ H.head headH <> H.body body <> footer
   where
     css u = H.link ! A.rel "stylesheet" ! A.href u
+    js  u = H.script ! A.src u $ mempty
+
     headH = mconcat 
-        [ H.title (toHtml title)
+        [ H.title (toHtml documentTitle)
         , css "//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css"
-        , H.style . toHtml $ T.concat
-            [ ".fetch{color:gray}"
-            , "footer{padding-top:40px;"
-            ,        "padding-bottom:40px;"
-            ,        "margin-top:20px;"
-            ,        "text-align:center;"
-            ,        "color:#777;"
-            ,        "border-top:1px solid #ddd"
-            ,       "}"
-            , ".description{margin-bottom:20px}"
-            , "table{margin-top:15px !important;margin-bottom:0px !important}"
-            , ".method{margin-bottom:20px;padding-bottom:10px;border-bottom:1px solid #ddd}"
-            , ".method:last-child{margin-bottom:0;padding-bottom:0;border-bottom:none}"
-            ]
+        , js  "//code.jquery.com/jquery-2.1.1.min.js"
+        , js  "//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"
+        , $(runIO (readFile "static/jquery.cookie-1.4.1.min.js") >>= \c -> [|H.script $ preEscapedToHtml (c::String)|])
+        , $(runIO (readFile "static/api-documentation.min.js")   >>= \c -> [|H.script $ preEscapedToHtml (c::String)|])
+        , $(runIO (readFile "static/api-documentation.min.css")     >>= \c -> [|H.style  $ preEscapedToHtml (c::String)|])
         ]
 
     htmlQR (Strict   r) = toHtml (show r)
@@ -178,26 +184,34 @@
         , queriesH qs
         ]
 
-    pathH (PathDoc r ms) =
+    dataToggle = attribute "data-toggle" " data-toggle=\""
+    dataTarget = attribute "data-target" " data-target=\""
+
+    pathH i (PathDoc r ms) =
         let (route, rdoc) = routeToHtml r
-            in H.div ! A.class_ "panel panel-default" $ mconcat
-            [ H.div ! A.class_ "panel-heading" $ mconcat
-                [ H.h3 ! A.class_ "panel-title" $ route
-                , rdoc
+        in H.div ! A.class_ "panel panel-default" $ mconcat
+            [ H.div ! A.class_ "panel-heading clearfix"
+                ! dataToggle "collapse" ! dataTarget (toValue $ "#collapse-" ++ show (i::Int)) $ mconcat
+                [ H.h3 ! A.class_ "panel-title pull-left" $ route
+                , H.div ! A.class_ "methods" $
+                    mcMap ((\m -> H.div ! A.class_ "pull-right" $ toHtml (T.decodeUtf8 m)) . fst) (reverse ms)
                 ]
-            , H.div ! A.class_ "panel-body" $ mcMap method ms
+            , H.div ! A.id (toValue $ "collapse-" ++ show i) ! A.class_ "panel-collapse collapse" $
+                rdoc <> (H.div ! A.class_ "panel-body" $ mcMap method ms)
             ]
 
-    groupH (g, p) = H.div $ mconcat [H.h2 $ toHtml g, mcMap pathH p]
+    groupH i (g, p) =
+        let (i', gs) = mapAccumL (\ii a -> (succ ii, pathH ii a)) i p
+        in (i', H.div ! A.id (toValue $ T.append "group-" g) $ mconcat [H.h2 $ toHtml g, mconcat gs])
 
-    doc (Documents n g) = mconcat
-        [ H.div $ mcMap pathH n
-        , mcMap groupH g
-        ]
+    doc (Documents n g) =
+        let (i, ng) = mapAccumL (\i' a -> (succ i', pathH  i' a)) (0::Int) n
+            (_, gs) = mapAccumL groupH i g
+        in (H.div ! A.id "no-group") (mconcat ng) <> mconcat gs
 
     body  = H.div ! A.class_ "container" $ mconcat
-        [ H.div ! A.class_ "page-header" $ H.h1 (toHtml title)
-        , maybe mempty (H.div ! A.class_ "description") desc
+        [ H.div ! A.class_ "page-header" $ H.h1 (toHtml documentTitle)
+        , maybe mempty (H.div ! A.class_ "description") documentDescription
         , doc docs
         ]
 
diff --git a/static/api-documentation.min.css b/static/api-documentation.min.css
new file mode 100644
--- /dev/null
+++ b/static/api-documentation.min.css
@@ -0,0 +1,1 @@
+.fetch{color:gray}footer{padding-top:30px;padding-bottom:50px;margin-top:20px;text-align:center;color:#777;border-top:1px solid #e5e5e5}.description{margin-bottom:20px}table{margin-top:15px !important;margin-bottom:0 !important}table.route-parameters{margin-top:0 !important;border-bottom:1px solid #ddd;background-color:#f5f5f5}.method{margin-bottom:20px;padding-bottom:10px;border-bottom:1px solid #ddd}.method:last-child{margin-bottom:0;padding-bottom:0;border-bottom:0}.methods div{margin-left:10px;color:#999}.panel-heading,h1{cursor:pointer}
diff --git a/static/api-documentation.min.js b/static/api-documentation.min.js
new file mode 100644
--- /dev/null
+++ b/static/api-documentation.min.js
@@ -0,0 +1,1 @@
+(function(){var unique=function(input){var u={},a=[];for(var i=0,l=input.length;i<l;++i){if(u.hasOwnProperty(input[i]))continue;a.push(input[i]);u[input[i]]=1}return a};var remove=function(input,v){var a=[];for(var i=0;i<input.length;i++){if(input[i]!=v)a.push(input[i])}return a};$.cookie.json=true;$.cookie.defaults.expires=365;$(function(){var current=$.cookie("open")||[];$(".collapse").on("show.bs.collapse",function(e){var cur=parseInt(e.target.id.slice(9));current.push(cur);current=unique(current);current.sort();$.cookie("open",current)}).on("hide.bs.collapse",function(e){var cur=parseInt(e.target.id.slice(9));current=remove(current,cur);$.cookie("open",current)});$("h1").click(function(){if(current.length==0){$(".collapse").each(function(i,e){$(e).collapse("show");var cur=parseInt(e.id.slice(9));current.push(cur)}).promise().done(function(){current=unique(current);current.sort();$.cookie("open",current)})}else{$(".collapse").each(function(i,e){$(e).collapse("hide")});current=[];$.cookie("open",[])}});for(var i=0;i<current.length;i++){$("#collapse-"+current[i]).addClass("in")}})})();
diff --git a/static/jquery.cookie-1.4.1.min.js b/static/jquery.cookie-1.4.1.min.js
new file mode 100644
--- /dev/null
+++ b/static/jquery.cookie-1.4.1.min.js
@@ -0,0 +1,2 @@
+/*! jquery.cookie v1.4.1 | MIT */
+!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):a("object"==typeof exports?require("jquery"):jQuery)}(function(a){function b(a){return h.raw?a:encodeURIComponent(a)}function c(a){return h.raw?a:decodeURIComponent(a)}function d(a){return b(h.json?JSON.stringify(a):String(a))}function e(a){0===a.indexOf('"')&&(a=a.slice(1,-1).replace(/\\"/g,'"').replace(/\\\\/g,"\\"));try{return a=decodeURIComponent(a.replace(g," ")),h.json?JSON.parse(a):a}catch(b){}}function f(b,c){var d=h.raw?b:e(b);return a.isFunction(c)?c(d):d}var g=/\+/g,h=a.cookie=function(e,g,i){if(arguments.length>1&&!a.isFunction(g)){if(i=a.extend({},h.defaults,i),"number"==typeof i.expires){var j=i.expires,k=i.expires=new Date;k.setTime(+k+864e5*j)}return document.cookie=[b(e),"=",d(g),i.expires?"; expires="+i.expires.toUTCString():"",i.path?"; path="+i.path:"",i.domain?"; domain="+i.domain:"",i.secure?"; secure":""].join("")}for(var l=e?void 0:{},m=document.cookie?document.cookie.split("; "):[],n=0,o=m.length;o>n;n++){var p=m[n].split("="),q=c(p.shift()),r=p.join("=");if(e&&e===q){l=f(r,g);break}e||void 0===(r=f(r))||(l[q]=r)}return l};h.defaults={},a.removeCookie=function(b,c){return void 0===a.cookie(b)?!1:(a.cookie(b,"",a.extend({},c,{expires:-1})),!a.cookie(b))}});
