diff --git a/Cargo.lock b/Cargo.lock
index 3948f30b..30075502 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -510,6 +510,13 @@ dependencies = [
"serde",
]
+[[package]]
+name = "experimental_component"
+version = "0.1.0"
+dependencies = [
+ "seed",
+]
+
[[package]]
name = "failure"
version = "0.1.8"
diff --git a/Cargo.toml b/Cargo.toml
index 6636dd55..c17658a4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -129,6 +129,7 @@ members = [
"examples/custom_elements",
"examples/drop_zone",
"examples/el_key",
+ "examples/experimental_component",
"examples/graphql",
"examples/i18n",
"examples/markdown",
diff --git a/examples/experimental_component/Cargo.toml b/examples/experimental_component/Cargo.toml
new file mode 100644
index 00000000..a1ec991d
--- /dev/null
+++ b/examples/experimental_component/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "experimental_component"
+version = "0.1.0"
+authors = ["glennsl"]
+edition = "2018"
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+seed = {path = "../../"}
diff --git a/examples/experimental_component/Makefile.toml b/examples/experimental_component/Makefile.toml
new file mode 100644
index 00000000..e188fabe
--- /dev/null
+++ b/examples/experimental_component/Makefile.toml
@@ -0,0 +1,27 @@
+extend = "../../Makefile.toml"
+
+# ---- BUILD ----
+
+[tasks.build]
+alias = "default_build"
+
+[tasks.build_release]
+alias = "default_build_release"
+
+# ---- START ----
+
+[tasks.start]
+alias = "default_start"
+
+[tasks.start_release]
+alias = "default_start_release"
+
+# ---- TEST ----
+
+[tasks.test_firefox]
+alias = "default_test_firefox"
+
+# ---- LINT ----
+
+[tasks.clippy]
+alias = "default_clippy"
diff --git a/examples/experimental_component/README.md b/examples/experimental_component/README.md
new file mode 100644
index 00000000..dc0f1701
--- /dev/null
+++ b/examples/experimental_component/README.md
@@ -0,0 +1,16 @@
+## Experimental component API example
+
+Demonstrates a component API that has:
+
+- Labeled required properties, ensured to be present at compile-time
+- Labeled optional properties
+- Polymorphic for all properties
+- A convenient consumer interface, through a stupidly simple macro that is very easy to understand
+
+---
+
+```bash
+cargo make start
+```
+
+Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser.
diff --git a/examples/experimental_component/index.html b/examples/experimental_component/index.html
new file mode 100644
index 00000000..0dfc2eb9
--- /dev/null
+++ b/examples/experimental_component/index.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+ Experimental component API example
+
+
+
+
+
+
+
+
diff --git a/examples/experimental_component/src/button.rs b/examples/experimental_component/src/button.rs
new file mode 100644
index 00000000..c2ff8d62
--- /dev/null
+++ b/examples/experimental_component/src/button.rs
@@ -0,0 +1,94 @@
+#![allow(dead_code)]
+
+use seed::{prelude::*, *};
+use std::borrow::Cow;
+use std::rc::Rc;
+
+pub struct Button {
+ pub label: S,
+}
+
+impl>> Button {
+ pub fn into_component(self) -> ButtonComponent {
+ ButtonComponent {
+ label: self.label.into(),
+ outlined: false,
+ disabled: false,
+ on_clicks: Vec::new(),
+ }
+ }
+}
+
+pub struct ButtonComponent {
+ label: Cow<'static, str>,
+ outlined: bool,
+ disabled: bool,
+ on_clicks: Vec Ms>>,
+}
+
+impl ButtonComponent {
+ pub const fn outlined(mut self, outlined: bool) -> Self {
+ self.outlined = outlined;
+ self
+ }
+
+ pub const fn disabled(mut self, disabled: bool) -> Self {
+ self.disabled = disabled;
+ self
+ }
+
+ pub fn on_click(mut self, on_click: impl FnOnce() -> Ms + Clone + 'static) -> Self {
+ self.on_clicks.push(Rc::new(move || on_click.clone()()));
+ self
+ }
+
+ pub fn into_node(self) -> Node {
+ let attrs = {
+ let mut attrs = attrs! {};
+
+ if self.disabled {
+ attrs.add(At::from("aria-disabled"), true);
+ attrs.add(At::TabIndex, -1);
+ attrs.add(At::Disabled, AtValue::None);
+ }
+
+ attrs
+ };
+
+ let css = {
+ let color = "teal";
+
+ let mut css = style! {
+ St::TextDecoration => "none",
+ };
+
+ if self.outlined {
+ css.merge(style! {
+ St::Color => color,
+ St::BackgroundColor => "transparent",
+ St::Border => format!("{} {} {}", px(2), "solid", color),
+ });
+ } else {
+ css.merge(style! { St::Color => "white", St::BackgroundColor => color });
+ };
+
+ if self.disabled {
+ css.merge(style! {St::Opacity => 0.5});
+ } else {
+ css.merge(style! {St::Cursor => "pointer"});
+ }
+
+ css
+ };
+
+ let mut button = button![css, attrs, self.label];
+
+ if !self.disabled {
+ for on_click in self.on_clicks {
+ button.add_event_handler(ev(Ev::Click, move |_| on_click()));
+ }
+ }
+
+ button
+ }
+}
diff --git a/examples/experimental_component/src/lib.rs b/examples/experimental_component/src/lib.rs
new file mode 100644
index 00000000..819f8748
--- /dev/null
+++ b/examples/experimental_component/src/lib.rs
@@ -0,0 +1,86 @@
+use seed::{prelude::*, *};
+
+mod button;
+use button::Button;
+
+// ------ ------
+// Init
+// ------ ------
+
+fn init(_: Url, _: &mut impl Orders) -> Model {
+ Model::default()
+}
+
+// ------ ------
+// Model
+// ------ ------
+
+type Model = i32;
+
+// ------ ------
+// Update
+// ------ ------
+
+enum Msg {
+ Increment(i32),
+ Decrement(i32),
+}
+
+#[allow(clippy::needless_pass_by_value)]
+fn update(msg: Msg, model: &mut Model, _: &mut impl Orders) {
+ match msg {
+ Msg::Increment(d) => *model += d,
+ Msg::Decrement(d) => *model -= d,
+ }
+}
+
+// ------ ------
+// View
+// ------ ------
+
+#[allow(clippy::trivially_copy_pass_by_ref)]
+fn view(model: &Model) -> Node {
+ div![
+ style! {
+ St::Display => "flex"
+ St::AlignItems => "center",
+ },
+ comp![Button { label: "-100" },
+ disabled => true,
+ on_click => || Msg::Decrement(100),
+ ],
+ comp![Button { label: "-10" }, on_click => || Msg::Decrement(10)],
+ comp![Button { label: "-1" },
+ outlined => true,
+ on_click => || Msg::Decrement(1),
+ ],
+ div![style! { St::Margin => "0 1em" }, model],
+ comp![Button { label: "+1" },
+ outlined => true,
+ on_click => || Msg::Increment(1),
+ ],
+ comp![Button { label: "+10" }, on_click => || Msg::Increment(10)],
+ comp![Button { label: "+100" },
+ disabled => true,
+ on_click => || Msg::Increment(100),
+ ]
+ ]
+}
+
+#[macro_export]
+macro_rules! comp {
+ ($init:expr, $($opt_field:ident => $opt_val:expr),* $(,)?) => {
+ $init.into_component()
+ $( .$opt_field($opt_val) )*
+ .into_node()
+ };
+}
+
+// ------ ------
+// Start
+// ------ ------
+
+#[wasm_bindgen(start)]
+pub fn start() {
+ App::start("app", init, update, view);
+}