diff --git a/Cargo.toml b/Cargo.toml
index 30843ed..062ff8f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -51,4 +51,4 @@ ssr = ["leptos/ssr", "leptos_meta/ssr"]
 hydrate = ["leptos/hydrate"]
 
 [workspace]
-members = ["demo", "examples/*"]
+members = ["demo", "demo_markdown", "examples/*"]
diff --git a/demo/Cargo.toml b/demo/Cargo.toml
index 622fca4..5db3a1b 100644
--- a/demo/Cargo.toml
+++ b/demo/Cargo.toml
@@ -20,6 +20,7 @@ icondata = { version = "0.1.0", features = [
     "AiSearchOutlined",
 ] }
 prisms = { git = "https://github.com/luoxiaozero/prisms", rev = "16d4d34b93fc20578ebf03137d54ecc7eafa4d4b" }
+demo_markdown = { path = "../demo_markdown" }
 
 [features]
 default = ["csr"]
diff --git a/demo/Trunk.toml b/demo/Trunk.toml
index 328d218..2227c00 100644
--- a/demo/Trunk.toml
+++ b/demo/Trunk.toml
@@ -5,7 +5,7 @@ public_url = "/thaw/"
 filehash = false
 
 [watch]
-watch = ["../src", "./src"]
+watch = ["../src", "./src", "../demo_markdown"]
 
 [serve]
 address = "127.0.0.1"
diff --git a/demo/src/app.rs b/demo/src/app.rs
index 19a8842..4d6063c 100644
--- a/demo/src/app.rs
+++ b/demo/src/app.rs
@@ -72,6 +72,7 @@ fn TheRouter(is_routing: RwSignal<bool>) -> impl IntoView {
                 <Route path="/switch" view=SwitchPage/>
                 <Route path="/tag" view=TagPage/>
                 <Route path="/upload" view=UploadPage/>
+                <Route path="/upload-md" view=UploadMdPage/>
                 <Route path="/loading-bar" view=LoadingBarPage/>
                 <Route path="/breadcrumb" view=BreadcrumbPage/>
                 <Route path="/layout" view=LayoutPage/>
diff --git a/demo/src/components/demo.rs b/demo/src/components/demo.rs
index f28f067..7b58782 100644
--- a/demo/src/components/demo.rs
+++ b/demo/src/components/demo.rs
@@ -4,6 +4,8 @@ use thaw::*;
 
 #[slot]
 pub struct DemoCode {
+    #[prop(default = true)]
+    is_highlight: bool,
     children: Children,
 }
 
@@ -37,7 +39,16 @@ pub fn Demo(demo_code: DemoCode, children: Children) -> impl IntoView {
         });
         style
     });
+    let content_class = create_memo(move |_| {
+        theme.with(|theme| {
+            format!(
+                "thaw-demo__content color-scheme--{}",
+                theme.common.color_scheme
+            )
+        })
+    });
 
+    let is_highlight = demo_code.is_highlight;
     let frag = (demo_code.children)();
     let mut html = String::new();
     for node in frag.nodes {
@@ -51,15 +62,30 @@ pub fn Demo(demo_code: DemoCode, children: Children) -> impl IntoView {
 
     view! {
         <Style id="leptos-thaw-prism-css">{prisms::prism_css!()}</Style>
+        <Style id="leptos-thaw-syntect-css">
+            {include_str!("./syntect-css.css")}
+        </Style>
         <Style id="leptos-thaw-prism-css-fix">
             ".token.operator {
                 background: hsla(0, 0%, 100%, 0) !important;
             }"
         </Style>
         <div style=move || style.get()>{children()}</div>
-        <div style=move || code_style.get()>
+        <div style=move || code_style.get() class=move || content_class.get()>
             <Code>
-                <pre style="margin: 0" inner_html=html></pre>
+                {
+                    if is_highlight {
+                        view! {
+                            <pre style="margin: 0" inner_html=html></pre>
+                        }
+                    } else {
+                        view! {
+                            <pre style="margin: 0">
+                                {html}
+                            </pre>
+                        }
+                    }
+                }
             </Code>
         </div>
     }
diff --git a/demo/src/components/syntect-css.css b/demo/src/components/syntect-css.css
new file mode 100644
index 0000000..847cc9d
--- /dev/null
+++ b/demo/src/components/syntect-css.css
@@ -0,0 +1,60 @@
+.thaw-demo__content pre {
+    font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas,
+        Liberation Mono, monospace;
+}
+
+/** https://github.com/AmjadHD/sublime_one_theme */
+
+/** light */
+
+.color-scheme--light .syntect-storage {
+    color: hsl(301, 63%, 40%);
+}
+
+.color-scheme--light .syntect-keyword.syntect-operator {
+    color: hsl(335, 95%, 62%);
+}
+
+.color-scheme--light .syntect-function,
+.color-scheme--light .syntect-macro {
+    color: hsl(221, 87%, 60%);
+}
+
+.color-scheme--light .syntect-support.syntect-type {
+    color: hsl(198, 99%, 37%);
+}
+
+.color-scheme--light .syntect-string {
+    color: hsl(119, 34%, 47%);
+}
+
+.color-scheme--light .syntect-placeholder {
+    color: hsl(41, 99%, 30%);
+}
+
+/** dark */
+
+.color-scheme--dark .syntect-storage {
+    color: hsl(286, 60%, 67%);
+}
+
+.color-scheme--dark .syntect-keyword.syntect-operator {
+    color: hsl(335, 95%, 62%);
+}
+
+.color-scheme--dark .syntect-function,
+.color-scheme--dark .syntect-macro {
+    color: hsl(207, 82%, 66%);
+}
+
+.color-scheme--dark .syntect-support.syntect-type {
+    color: hsl(187, 47%, 55%);
+}
+
+.color-scheme--dark .syntect-string {
+    color: hsl(95, 38%, 62%);
+}
+
+.color-scheme--dark .syntect-placeholder {
+    color: hsl(29, 54%, 61%);
+}
diff --git a/demo/src/pages/markdown.rs b/demo/src/pages/markdown.rs
new file mode 100644
index 0000000..3818021
--- /dev/null
+++ b/demo/src/pages/markdown.rs
@@ -0,0 +1,5 @@
+use crate::components::{Demo, DemoCode};
+use leptos::*;
+use thaw::*;
+
+demo_markdown::include_md! {}
diff --git a/demo/src/pages/mod.rs b/demo/src/pages/mod.rs
index 5932a1f..f935e25 100644
--- a/demo/src/pages/mod.rs
+++ b/demo/src/pages/mod.rs
@@ -20,6 +20,7 @@ mod input;
 mod input_number;
 mod layout;
 mod loading_bar;
+mod markdown;
 mod menu;
 mod message;
 mod mobile;
@@ -66,6 +67,7 @@ pub use input::*;
 pub use input_number::*;
 pub use layout::*;
 pub use loading_bar::*;
+pub use markdown::*;
 pub use menu::*;
 pub use message::*;
 pub use mobile::*;
diff --git a/demo_markdown/Cargo.toml b/demo_markdown/Cargo.toml
new file mode 100644
index 0000000..b9e5a59
--- /dev/null
+++ b/demo_markdown/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+publish = false
+name = "demo_markdown"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[lib]
+proc-macro = true
+
+[dependencies]
+quote = "1.0.33"
+comrak = "0.20.0"
+proc-macro2 = "1.0.71"
+syn = "2.0.43"
+syntect = "5.1.0"
\ No newline at end of file
diff --git a/demo_markdown/docs/upload/mod.md b/demo_markdown/docs/upload/mod.md
new file mode 100644
index 0000000..5c66ae7
--- /dev/null
+++ b/demo_markdown/docs/upload/mod.md
@@ -0,0 +1,54 @@
+# Upload
+
+```rust demo
+let message = use_message();
+let custom_request = move |file_list: FileList| {
+    message.create(
+        format!("Number of uploaded files: {}", file_list.length()),
+        MessageVariant::Success,
+        Default::default(),
+    );
+};
+
+view!{
+    <Upload>
+        <Button>
+            "upload"
+        </Button>
+    </Upload>
+}
+```
+
+### Drag to upload
+
+```rust demo
+let message = use_message();
+let custom_request = move |file_list: FileList| {
+    message.create(
+        format!("Number of uploaded files: {}", file_list.length()),
+        MessageVariant::Success,
+        Default::default(),
+    );
+};
+
+view! {
+    <Upload custom_request>
+        <UploadDragger>"Click or drag a file to this area to upload"</UploadDragger>
+    </Upload>
+}
+```
+
+### Upload Props
+
+| Name           | Type                             | Default              | Description                          |
+| -------------- | -------------------------------- | -------------------- | ------------------------------------ |
+| accept         | `MaybeSignal<String>`            | `Default::default()` | The accept type of upload.           |
+| multiple       | `MaybeSignal<bool>`              | `false`              | Allow multiple files to be selected. |
+| custom_request | `Option<Callback<FileList, ()>>` | `Default::default()` | Customize upload request.            |
+| children       | `Children`                       |                      | Upload's content.                    |
+
+### UploadDragger Props
+
+| Name     | Type       | Default | Description              |
+| -------- | ---------- | ------- | ------------------------ |
+| children | `Children` |         | UploadDragger's content. |
diff --git a/demo_markdown/src/lib.rs b/demo_markdown/src/lib.rs
new file mode 100644
index 0000000..0c51626
--- /dev/null
+++ b/demo_markdown/src/lib.rs
@@ -0,0 +1,55 @@
+mod markdown;
+
+use crate::markdown::parse_markdown;
+use proc_macro2::{Ident, Span};
+use quote::quote;
+use syn::ItemFn;
+
+#[proc_macro]
+pub fn include_md(_token_stream: proc_macro::TokenStream) -> proc_macro::TokenStream {
+    let file_list = vec![("UploadMdPage", include_str!("../docs/upload/mod.md"))];
+
+    let mut fn_list = vec![];
+
+    for (fn_name, file_str) in file_list {
+        let fn_name = Ident::new(fn_name, Span::call_site());
+
+        let (body, demos) = match parse_markdown(file_str) {
+            Ok(body) => body,
+            Err(err) => {
+                return quote!(compile_error!(#err)).into();
+            }
+        };
+
+        let demos: Vec<ItemFn> = demos
+            .into_iter()
+            .enumerate()
+            .map(|(index, demo)| {
+                format!(
+                    "#[component] fn Demo{}() -> impl IntoView {{ {} }}",
+                    index + 1,
+                    demo
+                )
+            })
+            .map(|demo| syn::parse_str::<ItemFn>(&demo).unwrap())
+            .collect();
+
+        fn_list.push(quote! {
+            #[component]
+            pub fn #fn_name() -> impl IntoView {
+                #(#demos)*
+
+                view! {
+                    <div style="width: 896px; margin: 0 auto;">
+                        #body
+                    </div>
+                }
+            }
+        });
+    }
+
+    quote! {
+        #(#fn_list)*
+    }
+    .into()
+}
diff --git a/demo_markdown/src/markdown/code_block.rs b/demo_markdown/src/markdown/code_block.rs
new file mode 100644
index 0000000..ee5f348
--- /dev/null
+++ b/demo_markdown/src/markdown/code_block.rs
@@ -0,0 +1,78 @@
+use comrak::nodes::NodeCodeBlock;
+use proc_macro2::{Ident, Span, TokenStream};
+use quote::quote;
+use std::sync::OnceLock;
+use syntect::{
+    html::{ClassStyle, ClassedHTMLGenerator},
+    parsing::SyntaxSet,
+    util::LinesWithEndings,
+};
+
+pub fn to_tokens(code_block: &NodeCodeBlock, demos: &mut Vec<String>) -> TokenStream {
+    let langs: Vec<&str> = code_block.info.split_ascii_whitespace().collect();
+    if langs.iter().any(|lang| lang == &"demo") {
+        demos.push(code_block.literal.clone());
+
+        let demo = Ident::new(&format!("Demo{}", demos.len()), Span::call_site());
+        let mut is_highlight = true;
+        let literal = langs
+            .iter()
+            .find(|lang| lang != &&"demo")
+            .map(|lang| highlight_to_html(&code_block.literal, lang))
+            .flatten()
+            .unwrap_or_else(|| {
+                is_highlight = false;
+                code_block.literal.clone()
+            });
+
+        quote! {
+            <Demo>
+                <#demo />
+                <DemoCode slot is_highlight=#is_highlight>
+                    #literal
+                </DemoCode>
+            </Demo>
+        }
+    } else {
+        let mut is_highlight = true;
+        let literal = langs
+            .first()
+            .map(|lang| highlight_to_html(&code_block.literal, lang))
+            .flatten()
+            .unwrap_or_else(|| {
+                is_highlight = false;
+                code_block.literal.clone()
+            });
+        quote! {
+            <Demo>
+                ""
+                <DemoCode slot is_highlight=#is_highlight>
+                    #literal
+                </DemoCode>
+            </Demo>
+        }
+    }
+}
+
+static SYNTAX_SET: OnceLock<SyntaxSet> = OnceLock::new();
+
+fn highlight_to_html(text: &str, syntax: &str) -> Option<String> {
+    let syntax_set = SYNTAX_SET.get_or_init(|| SyntaxSet::load_defaults_newlines());
+    let Some(syntax) = syntax_set.find_syntax_by_token(syntax) else {
+        return None;
+    };
+
+    let mut html_generator = ClassedHTMLGenerator::new_with_class_style(
+        syntax,
+        syntax_set,
+        ClassStyle::SpacedPrefixed { prefix: "syntect-" },
+    );
+
+    for line in LinesWithEndings::from(text) {
+        html_generator
+            .parse_html_for_line_which_includes_newline(line)
+            .expect(line);
+    }
+
+    Some(html_generator.finalize())
+}
diff --git a/demo_markdown/src/markdown/mod.rs b/demo_markdown/src/markdown/mod.rs
new file mode 100644
index 0000000..6fed5b9
--- /dev/null
+++ b/demo_markdown/src/markdown/mod.rs
@@ -0,0 +1,134 @@
+mod code_block;
+
+use comrak::{
+    nodes::{AstNode, NodeValue},
+    parse_document, Arena,
+};
+use proc_macro2::{Ident, Span, TokenStream};
+use quote::quote;
+
+pub fn parse_markdown(md_text: &str) -> Result<(TokenStream, Vec<String>), String> {
+    let mut demos: Vec<String> = vec![];
+
+    let arena = Arena::new();
+    let mut options = comrak::Options::default();
+    options.extension.table = true;
+
+    let root = parse_document(&arena, &md_text, &options);
+    let body = iter_nodes(root, &mut demos);
+    Ok((body, demos))
+}
+
+fn iter_nodes<'a>(node: &'a AstNode<'a>, demos: &mut Vec<String>) -> TokenStream {
+    let mut children = vec![];
+    for c in node.children() {
+        children.push(iter_nodes(c, demos));
+    }
+    match &node.data.borrow().value {
+        NodeValue::Document => quote!(#(#children)*),
+        NodeValue::FrontMatter(_) => quote!("FrontMatter todo!!!"),
+        NodeValue::BlockQuote => quote!("BlockQuote todo!!!"),
+        NodeValue::List(_) => quote!("List todo!!!"),
+        NodeValue::Item(_) => quote!("Item todo!!!"),
+        NodeValue::DescriptionList => quote!("DescriptionList todo!!!"),
+        NodeValue::DescriptionItem(_) => quote!("DescriptionItem todo!!!"),
+        NodeValue::DescriptionTerm => quote!("DescriptionTerm todo!!!"),
+        NodeValue::DescriptionDetails => quote!("DescriptionDetails todo!!!"),
+        NodeValue::CodeBlock(node_code_block) => code_block::to_tokens(node_code_block, demos),
+        NodeValue::HtmlBlock(_) => quote!("HtmlBlock todo!!!"),
+        NodeValue::Paragraph => quote!(
+            <p>
+                #(#children)*
+            </p >
+        ),
+        NodeValue::Heading(node_h) => {
+            let h = Ident::new(&format!("h{}", node_h.level), Span::call_site());
+            quote!(
+                <#h>
+                    #(#children)*
+                </#h>
+            )
+        }
+        NodeValue::ThematicBreak => quote!("ThematicBreak todo!!!"),
+        NodeValue::FootnoteDefinition(_) => quote!("FootnoteDefinition todo!!!"),
+        NodeValue::Table(_) => {
+            let header_index = {
+                let mut header_index = 0;
+                for (index, c) in node.children().enumerate() {
+                    let row = &c.data.borrow().value;
+                    let is_header = match *row {
+                        NodeValue::TableRow(header) => header,
+                        _ => panic!(),
+                    };
+                    if !is_header {
+                        header_index = index;
+                        break;
+                    }
+                }
+                header_index
+            };
+            let header_children: Vec<TokenStream> = children.drain(0..header_index).collect();
+
+            quote!(
+                <Table single_column=true>
+                    <thead>
+                        #(#header_children)*
+                    </thead>
+                    <tbody>
+                        #(#children)*
+                    </tbody>
+                </Table>
+            )
+        }
+        NodeValue::TableRow(_) => {
+            quote!(
+                <tr>
+                    #(#children)*
+                </tr>
+            )
+        }
+        NodeValue::TableCell => {
+            let row = &node.parent().unwrap().data.borrow().value;
+            let is_header = match *row {
+                NodeValue::TableRow(header) => header,
+                _ => panic!(),
+            };
+            if is_header {
+                quote!(
+                    <th>
+                        #(#children)*
+                    </th>
+                )
+            } else {
+                quote!(
+                    <td>
+                        #(#children)*
+                    </td>
+                )
+            }
+        }
+        NodeValue::Text(text) => {
+            let text = text.clone();
+            quote!(#text)
+        }
+        NodeValue::TaskItem(_) => quote!("TaskItem todo!!!"),
+        NodeValue::SoftBreak => quote!("\n"),
+        NodeValue::LineBreak => quote!(<br />),
+        NodeValue::Code(node_code) => {
+            let code = node_code.literal.clone();
+            quote!(
+                <Text code=true>
+                    #code
+                </Text>
+            )
+        }
+        NodeValue::HtmlInline(_) => quote!("HtmlInline todo!!!"),
+        NodeValue::Emph => quote!("Emph todo!!!"),
+        NodeValue::Strong => quote!("Strong todo!!!"),
+        NodeValue::Strikethrough => quote!("Strikethrough todo!!!"),
+        NodeValue::Superscript => quote!("Superscript todo!!!"),
+        NodeValue::Link(_) => quote!("Link todo!!!"),
+        NodeValue::Image(_) => quote!("Image todo!!!"),
+        NodeValue::FootnoteReference(_) => quote!("FootnoteReference todo!!!"),
+    }
+}