diff --git a/Cargo.lock b/Cargo.lock
index b4935c6..088865c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -113,7 +113,7 @@ dependencies = [
 
 [[package]]
 name = "respo"
-version = "0.0.14"
+version = "0.0.15"
 dependencies = [
  "cirru_parser",
  "js-sys",
diff --git a/README.md b/README.md
index 4041aed..a8a0325 100644
--- a/README.md
+++ b/README.md
@@ -8,11 +8,82 @@
 >
 > Respo was initially designed to work in a dynamic language with persistent data and HMR(hot code replacement), which is dramatically different from Rust. So this is more like an experiment.
 
-Docs https://docs.rs/respo
+- Docs https://docs.rs/respo
+- Live Demo https://r.tiye.me/Respo/respo.rs/
 
 ### Usage
 
-A preview example, to delare a store:
+Here is some preview of DOM syntax:
+
+```rust
+Ok(
+  div()
+    .class(ui_global())
+    .add_style(RespoStyle::default().padding(12.0).to_owned())
+    .add_children([
+      comp_counter(&states.pick("counter"), store.counted)?,
+      comp_panel(&states.pick("panel"))?,
+      comp_todolist(memo_caches, &states.pick("todolist"), &store.tasks)?,
+    ])
+    .to_owned(),
+)
+```
+
+CSS-in-Rust:
+
+```rust
+static_styles!(
+  style_remove_button,
+  (
+    "$0".to_owned(),
+    RespoStyle::default()
+      .width(CssSize::Px(16.0))
+      .height(CssSize::Px(16.0))
+      .margin(4.)
+      .cursor("pointer".to_owned())
+      .margin4(0.0, 0.0, 0.0, 16.0)
+      .color(CssColor::Hsl(0, 90, 90)),
+  ),
+  ("$0:hover".to_owned(), RespoStyle::default().color(CssColor::Hsl(0, 90, 80))),
+);
+```
+
+Builtin styles, [demonstrated](http://ui.respo-mvc.org/):
+
+| function           | usages                          |
+| ------------------ | ------------------------------- |
+| `ui_global`        | global styles                   |
+| `ui_fullscreen`    | fullscreen styles               |
+| `ui_button`        | button styles                   |
+| `ui_input`         | input styles                    |
+| `ui_textarea`      | textarea styles                 |
+| `ui_link`          | link styles                     |
+| `ui_flex`          | `flex:1` styles                 |
+| `ui_expand`        | `flex:1` styles with scrolls    |
+| `ui_center`        | flexbox center styles           |
+| `ui_row`           | flexbox row styles              |
+| `ui_column`        | flexbox column styles           |
+| `ui_row_center`    | flexbox row center styles       |
+| `ui_column_center` | flexbox column center styles    |
+| `ui_row_around`    | flexbox row around styles       |
+| `ui_column_around` | flexbox column around styles    |
+| `ui_row_evenly`    | flexbox row evenly styles       |
+| `ui_column_evenly` | flexbox column evenly styles    |
+| `ui_row_parted`    | flexbox row between styles      |
+| `ui_column_parted` | flexbox column between styles   |
+| `ui_row_middle`    | flexbox row between styles      |
+| `ui_column_middle` | flexbox column between styles   |
+| `ui_font_code`     | code font family                |
+| `ui_font_normal`   | normal font family(Hind)        |
+| `ui_font_fancy`    | fancy font family(Josefin Sans) |
+
+There are several dialog components in the demo. Syntax is not nice enough, so I'm not advertising it. But they work relatively good.
+
+For more components, read code in `src/app/`, they are just variants like `RespoNode::Component(..)`. It may be sugared in the future, not determined yet.
+
+### Store abstraction
+
+Declaring a store:
 
 ```rust
 #[derive(Debug, Clone, Deserialize, Serialize)]
@@ -48,7 +119,7 @@ impl RespoStore for Store {
 }
 ```
 
-To declare an app:
+Declaring an app:
 
 ```rust
 struct App {
@@ -94,7 +165,7 @@ impl RespoApp for App {
 }
 ```
 
-mount app:
+Mounting app:
 
 ```rust
 let app = App {
@@ -110,27 +181,6 @@ let app = App {
   app.render_loop().expect("app render");
 ```
 
-CSS-in-Rust:
-
-```rust
-static_styles!(
-  style_remove_button,
-  (
-    "$0".to_owned(),
-    RespoStyle::default()
-      .width(CssSize::Px(16.0))
-      .height(CssSize::Px(16.0))
-      .margin(4.)
-      .cursor("pointer".to_owned())
-      .margin4(0.0, 0.0, 0.0, 16.0)
-      .color(CssColor::Hsl(0, 90, 90)),
-  ),
-  ("$0:hover".to_owned(), RespoStyle::default().color(CssColor::Hsl(0, 90, 80))),
-);
-```
-
-For components, read code in `src/app/`, they are just variants like `RespoNode::Component(..)`. It may be sugared in the future, not decided yet.
-
 ### License
 
 Apache License 2.0 .
diff --git a/demo_respo/src/plugins.rs b/demo_respo/src/plugins.rs
index ee60488..a6ecd7c 100644
--- a/demo_respo/src/plugins.rs
+++ b/demo_respo/src/plugins.rs
@@ -1,13 +1,15 @@
 use std::fmt::Debug;
 
+use respo::RespoEvent;
 use respo::{space, ui::ui_row_parted, RespoStyle};
 use serde::{Deserialize, Serialize};
 
 use respo::{button, div, span, ui::ui_button, util, DispatchFn, RespoNode, StatesTree};
 
 use respo::dialog::{
-  AlertOptions, AlertPlugin, AlertPluginInterface, ConfirmOptions, ConfirmPlugin, ConfirmPluginInterface, ModalOptions, ModalPlugin,
-  ModalPluginInterface, ModalRenderer, PromptOptions, PromptPlugin, PromptPluginInterface, PromptValidator,
+  AlertOptions, AlertPlugin, AlertPluginInterface, ConfirmOptions, ConfirmPlugin, ConfirmPluginInterface, DrawerOptions, DrawerPlugin,
+  DrawerPluginInterface, DrawerRenderer, ModalOptions, ModalPlugin, ModalPluginInterface, ModalRenderer, PromptOptions, PromptPlugin,
+  PromptPluginInterface, PromptValidator,
 };
 
 use super::store::*;
@@ -147,6 +149,48 @@ pub fn comp_plugins_demo(states: &StatesTree) -> Result<RespoNode<ActionOp>, Str
     }
   };
 
+  // declare drawer
+
+  let drawer_plugin = DrawerPlugin::new(
+    states.pick("drawer"),
+    DrawerOptions {
+      title: Some(String::from("Modal demo")),
+      render: DrawerRenderer::new(|close_drawer: _| {
+        let handler = move |_e: _, dispatch: DispatchFn<ActionOp>| {
+          respo::util::log!("on modal handle");
+          close_drawer(dispatch)
+        };
+        Ok(
+          div()
+            .style(RespoStyle::default().padding(8.0).to_owned())
+            .children([
+              div()
+                .children([span().inner_text("content in custom drawer").to_owned()])
+                .to_owned(),
+              div()
+                .class(ui_row_parted())
+                .children([span(), button().class(ui_button()).inner_text("close").on_click(handler).to_owned()])
+                .to_owned(),
+            ])
+            .to_owned(),
+        )
+      }),
+      ..DrawerOptions::default()
+    },
+  )?
+  .share_with_ref();
+
+  let on_drawer = {
+    let drawer_plugin = drawer_plugin.clone();
+    move |e: RespoEvent, dispatch: DispatchFn<_>| -> Result<(), String> {
+      util::log!("click {:?}", e);
+
+      drawer_plugin.show(dispatch)?;
+
+      Ok(())
+    }
+  };
+
   Ok(
     div()
       .children([
@@ -168,12 +212,19 @@ pub fn comp_plugins_demo(states: &StatesTree) -> Result<RespoNode<ActionOp>, Str
               .inner_text("Try Custom Modal")
               .on_click(on_modal)
               .to_owned(),
+            space(Some(8), None),
+            button()
+              .class(ui_button())
+              .inner_text("Try Custom Drawer")
+              .on_click(on_drawer)
+              .to_owned(),
           ])
           .to_owned(),
         alert_plugin.render()?,
         confirm_plugin.render()?,
         prompt_plugin.render()?,
         modal_plugin.render()?,
+        drawer_plugin.render()?,
       ])
       .to_owned(),
   )
diff --git a/respo/Cargo.toml b/respo/Cargo.toml
index f6e681f..7fc3c72 100644
--- a/respo/Cargo.toml
+++ b/respo/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "respo"
-version = "0.0.14"
+version = "0.0.15"
 edition = "2021"
 description = "a tiny virtual DOM library migrated from ClojureScript"
 license = "Apache-2.0"
diff --git a/respo/src/dialog.rs b/respo/src/dialog.rs
index 001be5d..2f06727 100644
--- a/respo/src/dialog.rs
+++ b/respo/src/dialog.rs
@@ -2,6 +2,7 @@
 
 mod alert;
 mod confirm;
+mod drawer;
 mod modal;
 mod prompt;
 
@@ -17,6 +18,7 @@ pub(crate) const BUTTON_NAME: &str = "dialog-button";
 
 pub use alert::{AlertOptions, AlertPlugin, AlertPluginInterface};
 pub use confirm::{ConfirmOptions, ConfirmPlugin, ConfirmPluginInterface};
+pub use drawer::{DrawerOptions, DrawerPlugin, DrawerPluginInterface, DrawerRenderer};
 pub use modal::{ModalOptions, ModalPlugin, ModalPluginInterface, ModalRenderer};
 pub use prompt::{PromptOptions, PromptPlugin, PromptPluginInterface, PromptValidator};
 
@@ -71,7 +73,7 @@ pub(crate) fn effect_fade(args: Vec<RespoEffectArg>, effect_type: RespoEffectTyp
               let card = cloned.first_child().unwrap();
               let card_style = card.dyn_ref::<HtmlElement>().unwrap().style();
               card_style.set_property("transition-duration", "240ms").unwrap();
-              card_style.set_property("transform", "scale(0.94) translate(0px,-20px)").unwrap();
+              card_style.set_property("transform", "translate(100%,0px)").unwrap();
             });
             window
               .set_timeout_with_callback_and_timeout_and_arguments_0(immediate_call.as_ref().unchecked_ref(), 10)
@@ -98,12 +100,12 @@ pub(crate) fn effect_fade(args: Vec<RespoEffectArg>, effect_type: RespoEffectTyp
         let style = target.dyn_ref::<HtmlElement>().unwrap().style();
         let card_style = target.first_child().unwrap().dyn_ref::<HtmlElement>().unwrap().style();
         style.set_property("opacity", "0").unwrap();
-        card_style.set_property("transform", "scale(0.94)").unwrap();
+        card_style.set_property("transform", "translate(100%, 0px)").unwrap();
         let call = Closure::once(move || {
           style.set_property("transition-duration", "240ms").unwrap();
           card_style.set_property("transition-duration", "240ms").unwrap();
           style.set_property("opacity", "1").unwrap();
-          card_style.set_property("transform", "scale(1) translate(0px,0px)").unwrap();
+          card_style.set_property("transform", "translate(0px,0px)").unwrap();
         });
         let window = web_sys::window().unwrap();
         window
@@ -126,7 +128,6 @@ static_styles!(
       .background_color(CssColor::Hsla(0.0, 30.0, 10.0, 0.6))
       .position(CssPosition::Fixed)
       .z_index(999)
-      .padding(16.0)
   )
 );
 
@@ -136,14 +137,17 @@ static_styles!(
     "$0".to_owned(),
     RespoStyle::default()
       .background_color(CssColor::Hsl(0, 0, 100))
-      .max_width(CssSize::Px(600.0))
-      .width(CssSize::Percent(100.))
-      .max_height(CssSize::Vh(80.0))
+      .max_width(CssSize::Vw(50.0))
+      .width(CssSize::Px(400.))
+      .height(CssSize::Vh(100.0))
       .overflow(CssOverflow::Auto)
-      .border_radius(3.0)
       .color(CssColor::Hsl(0, 0, 0))
-      .insert("margin", "auto".to_owned())
-      .padding(16.0)
+      .top(CssSize::Px(0.))
+      .right(CssSize::Px(0.))
+      .bottom(CssSize::Px(0.))
+      .position(CssPosition::Absolute)
+      .box_shadow(-2., 0., 12., 0., CssColor::Hsla(0., 0., 0., 0.2))
+      .transform_property("transform, opacity".to_owned())
   )
 );
 
diff --git a/respo/src/dialog/drawer.rs b/respo/src/dialog/drawer.rs
new file mode 100644
index 0000000..0a3281a
--- /dev/null
+++ b/respo/src/dialog/drawer.rs
@@ -0,0 +1,222 @@
+use std::fmt::Debug;
+
+use std::marker::PhantomData;
+use std::rc::Rc;
+
+use serde::{Deserialize, Serialize};
+
+use crate::dialog::{css_backdrop, css_card};
+use crate::ui::{ui_center, ui_column, ui_fullscreen, ui_global};
+
+use crate::{div, space, span, CssLineHeight, CssPosition, DispatchFn, RespoAction, RespoEvent, RespoNode, RespoStyle, StatesTree};
+
+use crate::dialog::effect_fade;
+
+/// The options for custom drawer.
+#[derive(Debug, Clone, Default)]
+pub struct DrawerOptions<T>
+where
+  T: Debug + Clone,
+{
+  /// inline style for backdrop
+  pub backdrop_style: RespoStyle,
+  /// inline style for card
+  pub card_style: RespoStyle,
+  /// title of the drawer, defaults to `drawer`
+  pub title: Option<String>,
+  /// render body
+  pub render: DrawerRenderer<T>,
+}
+
+type DrawerRendererFn<T> = dyn Fn(Rc<dyn Fn(DispatchFn<T>) -> Result<(), String>>) -> Result<RespoNode<T>, String>;
+
+/// wraps render function
+#[derive(Clone)]
+pub struct DrawerRenderer<T>(Rc<DrawerRendererFn<T>>)
+where
+  T: Debug + Clone;
+
+impl<T> Debug for DrawerRenderer<T>
+where
+  T: Debug + Clone,
+{
+  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+    write!(f, "(&DrawerRenderer ..)")
+  }
+}
+
+impl<T> Default for DrawerRenderer<T>
+where
+  T: Debug + Clone,
+{
+  fn default() -> Self {
+    Self(Rc::new(|_close: _| Ok(div())))
+  }
+}
+
+impl<T> DrawerRenderer<T>
+where
+  T: Debug + Clone,
+{
+  pub fn new<V>(renderer: V) -> Self
+  where
+    V: Fn(Rc<dyn Fn(DispatchFn<T>) -> Result<(), String>>) -> Result<RespoNode<T>, String> + 'static,
+  {
+    Self(Rc::new(renderer))
+  }
+
+  pub fn run<V>(&self, close: V) -> Result<RespoNode<T>, String>
+  where
+    V: Fn(DispatchFn<T>) -> Result<(), String> + 'static,
+  {
+    (self.0)(Rc::new(close))
+  }
+}
+
+fn comp_drawer<T, U>(options: DrawerOptions<T>, show: bool, on_close: U) -> Result<RespoNode<T>, String>
+where
+  U: Fn(DispatchFn<T>) -> Result<(), String> + 'static,
+  T: Clone + Debug,
+{
+  let close = Rc::new(on_close);
+  let close2 = close.clone();
+
+  Ok(
+    RespoNode::new_component(
+      "drawer",
+      div()
+        .style(RespoStyle::default().position(CssPosition::Absolute).to_owned())
+        .children([if show {
+          div()
+            .class_list(&[ui_fullscreen(), ui_center(), css_backdrop()])
+            .style(options.backdrop_style)
+            .on_click(move |e, dispatch| -> Result<(), String> {
+              if let RespoEvent::Click { original_event, .. } = e {
+                // stop propagation to prevent closing the drawer
+                original_event.stop_propagation();
+              }
+              close(dispatch)?;
+              Ok(())
+            })
+            .children([div()
+              .class_list(&[ui_column(), ui_global(), css_card()])
+              .style(RespoStyle::default().padding(0.0).line_height(CssLineHeight::Px(32.0)).to_owned())
+              .style(options.card_style)
+              .on_click(move |e, _dispatch| -> Result<(), String> {
+                // nothing to do
+                if let RespoEvent::Click { original_event, .. } = e {
+                  // stop propagation to prevent closing the drawer
+                  original_event.stop_propagation();
+                }
+                Ok(())
+              })
+              .children([div()
+                .class(ui_column())
+                .children([
+                  div()
+                    .class(ui_center())
+                    .children([span().inner_text(options.title.unwrap_or_else(|| "Drawer".to_owned())).to_owned()])
+                    .to_owned(),
+                  space(None, Some(8)),
+                  options.render.run(move |dispatch| -> Result<(), String> {
+                    let close = close2.clone();
+                    close(dispatch)?;
+                    Ok(())
+                  })?,
+                ])
+                .to_owned()])
+              .to_owned()])
+            .to_owned()
+        } else {
+          span().attribute("data-name", "placeholder").to_owned()
+        }])
+        .to_owned(),
+    )
+    // .effect(&[show], effect_focus)
+    .effect(&[show], effect_fade)
+    .share_with_ref(),
+  )
+}
+
+/// provides the interfaces to component of custom drawer dialog
+pub trait DrawerPluginInterface<T>
+where
+  T: Debug + Clone + RespoAction,
+{
+  /// renders UI
+  fn render(&self) -> Result<RespoNode<T>, String>
+  where
+    T: Clone + Debug;
+  /// to show drawer
+  fn show(&self, dispatch: DispatchFn<T>) -> Result<(), String>;
+  /// to close drawer
+  fn close(&self, dispatch: DispatchFn<T>) -> Result<(), String>;
+
+  fn new(states: StatesTree, options: DrawerOptions<T>) -> Result<Self, String>
+  where
+    Self: std::marker::Sized;
+
+  /// share it with `Rc`
+  fn share_with_ref(&self) -> Rc<Self>;
+}
+
+#[derive(Debug, Clone, Default, Serialize, Deserialize)]
+struct DrawerPluginState {
+  show: bool,
+}
+
+/// a drawer that you can render you down card body
+#[derive(Debug, Clone)]
+pub struct DrawerPlugin<T>
+where
+  T: Clone + Debug,
+{
+  state: DrawerPluginState,
+  options: DrawerOptions<T>,
+  /// tracking content to display
+  cursor: Vec<String>,
+  phantom: PhantomData<T>,
+}
+
+impl<T> DrawerPluginInterface<T> for DrawerPlugin<T>
+where
+  T: Clone + Debug + RespoAction,
+{
+  fn render(&self) -> Result<RespoNode<T>, String> {
+    let cursor = self.cursor.clone();
+
+    comp_drawer(self.options.to_owned(), self.state.show, move |dispatch: DispatchFn<_>| {
+      let s = DrawerPluginState { show: false };
+      dispatch.run_state(&cursor, s)?;
+      Ok(())
+    })
+  }
+  fn show(&self, dispatch: DispatchFn<T>) -> Result<(), String> {
+    let s = DrawerPluginState { show: true };
+    dispatch.run_state(&self.cursor, s)?;
+    Ok(())
+  }
+  fn close(&self, dispatch: DispatchFn<T>) -> Result<(), String> {
+    let s = DrawerPluginState { show: false };
+    dispatch.run_state(&self.cursor, s)?;
+    Ok(())
+  }
+
+  fn new(states: StatesTree, options: DrawerOptions<T>) -> Result<Self, String> {
+    let cursor = states.path();
+    let state: DrawerPluginState = states.data.cast_or_default()?;
+
+    let instance = Self {
+      state,
+      options,
+      cursor,
+      phantom: PhantomData,
+    };
+
+    Ok(instance)
+  }
+
+  fn share_with_ref(&self) -> Rc<Self> {
+    Rc::new(self.clone())
+  }
+}
diff --git a/respo/src/respo/css.rs b/respo/src/respo/css.rs
index 21d1044..3801533 100644
--- a/respo/src/respo/css.rs
+++ b/respo/src/respo/css.rs
@@ -302,7 +302,7 @@ impl Display for CssSize {
       Self::Px(v) => write!(f, "{}px", v),
       Self::Percent(v) => write!(f, "{}%", v),
       Self::Vw(v) => write!(f, "{}vw", v),
-      Self::Vh(v) => write!(f, "{}wh", v),
+      Self::Vh(v) => write!(f, "{}vh", v),
       Self::Custom(v) => write!(f, "{}", v),
     }
   }