diff --git a/examples/basic.rs b/examples/basic.rs
index cd167c3..1b217ea 100644
--- a/examples/basic.rs
+++ b/examples/basic.rs
@@ -1,7 +1,7 @@
 use based::request::{RequestContext, StringResponse};
 use based::{
     get_pg,
-    page::{Shell, render_page},
+    ui::{Shell, render_page},
 };
 use maud::html;
 use rocket::get;
diff --git a/examples/ui.rs b/examples/ui.rs
new file mode 100644
index 0000000..9683986
--- /dev/null
+++ b/examples/ui.rs
@@ -0,0 +1,42 @@
+use based::request::{RequestContext, StringResponse};
+use based::ui::{Shell, render_page};
+use maud::Render;
+use maud::html;
+use rocket::get;
+use rocket::routes;
+
+use based::ui::appbar::AppBar;
+
+#[get("/")]
+pub async fn index_page(ctx: RequestContext) -> StringResponse {
+    let content = AppBar("MyApp", None).render();
+
+    let content = html!(
+        h1 { "Hello World!" };
+
+        (content)
+
+    );
+
+    render_page(
+        content,
+        "Hello World",
+        ctx,
+        &Shell::new(
+            html! {
+                script src="https://cdn.tailwindcss.com" {};
+            },
+            html! {},
+            Some(String::new()),
+        ),
+    )
+    .await
+}
+
+#[rocket::launch]
+async fn launch() -> _ {
+    // Logging
+    env_logger::init();
+
+    rocket::build().mount("/", routes![index_page])
+}
diff --git a/src/lib.rs b/src/lib.rs
index 44842e3..bfe93a5 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -4,9 +4,9 @@ pub mod auth;
 pub mod format;
 #[cfg(feature = "htmx")]
 pub mod htmx;
-pub mod page;
 pub mod request;
 pub mod result;
+pub mod ui;
 
 // TODO : CORS?
 
diff --git a/src/ui/appbar.rs b/src/ui/appbar.rs
new file mode 100644
index 0000000..10f20e1
--- /dev/null
+++ b/src/ui/appbar.rs
@@ -0,0 +1,68 @@
+use maud::{Markup, Render};
+
+use crate::auth::User;
+
+use crate::ui::basic::*;
+
+use super::UIWidget;
+
+#[allow(non_snake_case)]
+pub fn AppBar(name: &str, user: Option<User>) -> AppBarWidget {
+    AppBarWidget {
+        name: name.to_owned(),
+        user,
+    }
+}
+
+pub struct AppBarWidget {
+    name: String,
+    user: Option<User>,
+}
+
+impl Render for AppBarWidget {
+    fn render(&self) -> Markup {
+        self.render_with_class("")
+    }
+}
+
+impl UIWidget for AppBarWidget {
+    fn can_inherit(&self) -> bool {
+        false
+    }
+
+    fn render_with_class(&self, _: &str) -> Markup {
+        Padding(Shadow::medium(Background(
+            Gray::_800,
+            Header(
+                Padding(
+                    Flex(
+                        Div()
+                            .vanish()
+                            .add(
+                                Flex(Link(
+                                    "/",
+                                    Div()
+                                        .vanish()
+                                        .add(Sized(
+                                            10,
+                                            10,
+                                            RoundedMedium(Image("/favicon").alt("Logo")),
+                                        ))
+                                        .add(Span(&self.name).semibold().xl().white()),
+                                ))
+                                .items_center()
+                                .space_x(2),
+                            )
+                            .add_some(self.user.as_ref(), |user| Text(&user.username).white()),
+                    )
+                    .group()
+                    .justify(Justify::Between)
+                    .items_center(),
+                )
+                .x(6),
+            ),
+        )))
+        .y(2)
+        .render()
+    }
+}
diff --git a/src/ui/aspect.rs b/src/ui/aspect.rs
new file mode 100644
index 0000000..0158951
--- /dev/null
+++ b/src/ui/aspect.rs
@@ -0,0 +1,54 @@
+use maud::{Markup, Render, html};
+
+use super::UIWidget;
+
+pub struct Aspect {
+    kind: u8,
+    inner: Box<dyn UIWidget>,
+}
+
+impl Aspect {
+    pub fn auto<T: UIWidget + 'static>(inner: T) -> Self {
+        Self {
+            kind: 0,
+            inner: Box::new(inner),
+        }
+    }
+
+    pub fn square<T: UIWidget + 'static>(inner: T) -> Self {
+        Self {
+            kind: 1,
+            inner: Box::new(inner),
+        }
+    }
+
+    pub fn video<T: UIWidget + 'static>(inner: T) -> Self {
+        Self {
+            kind: 2,
+            inner: Box::new(inner),
+        }
+    }
+}
+
+impl Render for Aspect {
+    fn render(&self) -> Markup {
+        let class = match self.kind {
+            0 => "aspect-auto",
+            1 => "aspect-square",
+            2 => "aspect-video",
+            _ => "",
+        };
+
+        if self.inner.as_ref().can_inherit() {
+            html! {
+                (self.inner.as_ref().render_with_class(class))
+            }
+        } else {
+            html! {
+                div class=(class) {
+                    (self.inner.as_ref())
+                }
+            }
+        }
+    }
+}
diff --git a/src/ui/background.rs b/src/ui/background.rs
new file mode 100644
index 0000000..5a431c4
--- /dev/null
+++ b/src/ui/background.rs
@@ -0,0 +1,66 @@
+use super::UIWidget;
+use maud::{Markup, Render, html};
+
+pub trait UIColor {
+    fn color_class(&self) -> &str;
+}
+
+pub enum Blue {
+    _500,
+}
+
+impl UIColor for Blue {
+    fn color_class(&self) -> &str {
+        match self {
+            Blue::_500 => "blue-500",
+        }
+    }
+}
+
+pub enum Gray {
+    _800,
+}
+
+impl UIColor for Gray {
+    fn color_class(&self) -> &str {
+        match self {
+            Gray::_800 => "gray-800",
+        }
+    }
+}
+
+#[allow(non_snake_case)]
+pub fn Background<T: UIWidget + 'static, C: UIColor + 'static>(
+    color: C,
+    inner: T,
+) -> BackgroundWidget {
+    BackgroundWidget(Box::new(inner), Box::new(color))
+}
+
+pub struct BackgroundWidget(Box<dyn UIWidget>, Box<dyn UIColor>);
+
+impl Render for BackgroundWidget {
+    fn render(&self) -> Markup {
+        self.render_with_class("")
+    }
+}
+
+impl UIWidget for BackgroundWidget {
+    fn can_inherit(&self) -> bool {
+        true
+    }
+
+    fn render_with_class(&self, class: &str) -> Markup {
+        if self.0.as_ref().can_inherit() {
+            self.0
+                .as_ref()
+                .render_with_class(&format!("bg-{} {class}", self.1.color_class()))
+        } else {
+            html! {
+                div class=(format!("bg-{} {class}", self.1.color_class())) {
+                    (self.0.as_ref())
+                }
+            }
+        }
+    }
+}
diff --git a/src/ui/container.rs b/src/ui/container.rs
new file mode 100644
index 0000000..5220c77
--- /dev/null
+++ b/src/ui/container.rs
@@ -0,0 +1,37 @@
+use maud::{Markup, Render, html};
+
+use super::UIWidget;
+
+#[allow(non_snake_case)]
+/// A component for fixing an element's width to the current breakpoint.
+pub fn Container<T: UIWidget + 'static>(inner: T) -> ContainerWidget {
+    ContainerWidget(Box::new(inner))
+}
+
+pub struct ContainerWidget(Box<dyn UIWidget>);
+
+impl Render for ContainerWidget {
+    fn render(&self) -> Markup {
+        self.render_with_class("")
+    }
+}
+
+impl UIWidget for ContainerWidget {
+    fn can_inherit(&self) -> bool {
+        true
+    }
+
+    fn render_with_class(&self, class: &str) -> Markup {
+        if self.0.as_ref().can_inherit() {
+            self.0
+                .as_ref()
+                .render_with_class(&format!("container {class}"))
+        } else {
+            html! {
+                div class=(format!("container {class}")) {
+                    (self.0.as_ref())
+                }
+            }
+        }
+    }
+}
diff --git a/src/ui/div.rs b/src/ui/div.rs
new file mode 100644
index 0000000..8a40ad1
--- /dev/null
+++ b/src/ui/div.rs
@@ -0,0 +1,63 @@
+use maud::{Markup, Render, html};
+
+use super::UIWidget;
+
+#[allow(non_snake_case)]
+pub fn Div() -> DivWidget {
+    DivWidget(Vec::new(), false)
+}
+
+pub struct DivWidget(Vec<Box<dyn UIWidget>>, bool);
+
+impl DivWidget {
+    pub fn add<T: UIWidget + 'static>(mut self, element: T) -> Self {
+        self.0.push(Box::new(element));
+        self
+    }
+
+    pub fn add_some<T: UIWidget + 'static, X, U: Fn(&X) -> T>(
+        mut self,
+        option: Option<&X>,
+        then: U,
+    ) -> Self {
+        if let Some(val) = option {
+            self.0.push(Box::new(then(val)));
+        }
+        self
+    }
+
+    pub fn vanish(mut self) -> Self {
+        self.1 = true;
+        self
+    }
+}
+
+impl Render for DivWidget {
+    fn render(&self) -> Markup {
+        self.render_with_class("")
+    }
+}
+
+impl UIWidget for DivWidget {
+    fn can_inherit(&self) -> bool {
+        false
+    }
+
+    fn render_with_class(&self, _: &str) -> Markup {
+        if self.1 {
+            html! {
+                @for e in &self.0 {
+                    (e.as_ref())
+                }
+            }
+        } else {
+            html! {
+                div {
+                    @for e in &self.0 {
+                        (e.as_ref())
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/src/ui/flex.rs b/src/ui/flex.rs
new file mode 100644
index 0000000..6ed2aa1
--- /dev/null
+++ b/src/ui/flex.rs
@@ -0,0 +1,78 @@
+use super::UIWidget;
+use maud::{Markup, Render, html};
+
+#[allow(non_snake_case)]
+pub fn Flex<T: UIWidget + 'static>(inner: T) -> FlexWidget {
+    FlexWidget(Box::new(inner), vec![], false)
+}
+
+pub enum Justify {
+    Center,
+    Between,
+}
+
+pub struct FlexWidget(Box<dyn UIWidget>, Vec<String>, bool);
+
+impl Render for FlexWidget {
+    fn render(&self) -> Markup {
+        self.render_with_class("")
+    }
+}
+
+impl FlexWidget {
+    pub fn full_center(mut self) -> Self {
+        self.1.push("items-center".to_owned());
+        self.1.push("justify-center".to_owned());
+        self
+    }
+
+    pub fn group(mut self) -> Self {
+        self.2 = true;
+        self
+    }
+
+    pub fn justify(mut self, value: Justify) -> Self {
+        let class = match value {
+            Justify::Center => "justify-center".to_owned(),
+            Justify::Between => "justify-between".to_owned(),
+        };
+
+        self.1.push(class);
+        self
+    }
+
+    pub fn space_x(mut self, x: u32) -> Self {
+        self.1.push(format!("space-x-{x}"));
+        self
+    }
+
+    pub fn items_center(mut self) -> Self {
+        self.1.push("items-center".to_owned());
+        self
+    }
+
+    pub fn gap(mut self, amount: u32) -> Self {
+        self.1.push(format!("gap-{amount}"));
+        self
+    }
+}
+
+impl UIWidget for FlexWidget {
+    fn can_inherit(&self) -> bool {
+        true
+    }
+
+    fn render_with_class(&self, class: &str) -> Markup {
+        if self.0.as_ref().can_inherit() && !self.2 {
+            self.0
+                .as_ref()
+                .render_with_class(&format!("flex {} {class}", self.1.join(" ")))
+        } else {
+            html! {
+                div class=(format!("flex {} {class}", self.1.join(" "))) {
+                    (self.0.as_ref())
+                }
+            }
+        }
+    }
+}
diff --git a/src/ui/header.rs b/src/ui/header.rs
new file mode 100644
index 0000000..2751bb1
--- /dev/null
+++ b/src/ui/header.rs
@@ -0,0 +1,30 @@
+use maud::{Markup, Render, html};
+
+use super::UIWidget;
+
+#[allow(non_snake_case)]
+pub fn Header<T: UIWidget + 'static>(inner: T) -> HeaderWidget {
+    HeaderWidget(Box::new(inner))
+}
+
+pub struct HeaderWidget(Box<dyn UIWidget>);
+
+impl Render for HeaderWidget {
+    fn render(&self) -> Markup {
+        self.render_with_class("")
+    }
+}
+
+impl UIWidget for HeaderWidget {
+    fn can_inherit(&self) -> bool {
+        true
+    }
+
+    fn render_with_class(&self, class: &str) -> Markup {
+        html! {
+            header class=(class) {
+                (self.0.as_ref())
+            }
+        }
+    }
+}
diff --git a/src/ui/image.rs b/src/ui/image.rs
new file mode 100644
index 0000000..da0eb5e
--- /dev/null
+++ b/src/ui/image.rs
@@ -0,0 +1,40 @@
+use super::UIWidget;
+use maud::{Markup, Render, html};
+
+#[allow(non_snake_case)]
+pub fn Image(src: &str) -> ImageWidget {
+    ImageWidget {
+        src: src.to_owned(),
+        alt: String::new(),
+    }
+}
+
+pub struct ImageWidget {
+    src: String,
+    alt: String,
+}
+
+impl Render for ImageWidget {
+    fn render(&self) -> Markup {
+        self.render_with_class("")
+    }
+}
+
+impl ImageWidget {
+    pub fn alt(mut self, alt: &str) -> Self {
+        self.alt = alt.to_owned();
+        self
+    }
+}
+
+impl UIWidget for ImageWidget {
+    fn can_inherit(&self) -> bool {
+        true
+    }
+
+    fn render_with_class(&self, class: &str) -> Markup {
+        html! {
+            img src=(self.src) alt=(self.alt) class=(class) {};
+        }
+    }
+}
diff --git a/src/ui/link.rs b/src/ui/link.rs
new file mode 100644
index 0000000..f5d4883
--- /dev/null
+++ b/src/ui/link.rs
@@ -0,0 +1,31 @@
+use maud::{Markup, Render, html};
+
+use super::UIWidget;
+
+#[allow(non_snake_case)]
+/// A component for fixing an element's width to the current breakpoint.
+pub fn Link<T: UIWidget + 'static>(reference: &str, inner: T) -> LinkWidget {
+    LinkWidget(Box::new(inner), reference.to_owned())
+}
+
+pub struct LinkWidget(Box<dyn UIWidget>, String);
+
+impl Render for LinkWidget {
+    fn render(&self) -> Markup {
+        self.render_with_class("")
+    }
+}
+
+impl UIWidget for LinkWidget {
+    fn can_inherit(&self) -> bool {
+        true
+    }
+
+    fn render_with_class(&self, class: &str) -> Markup {
+        html! {
+            a class=(class) href=(self.1) {
+                (self.0.as_ref())
+            }
+        }
+    }
+}
diff --git a/src/page/mod.rs b/src/ui/mod.rs
similarity index 74%
rename from src/page/mod.rs
rename to src/ui/mod.rs
index f9091ef..3114828 100644
--- a/src/page/mod.rs
+++ b/src/ui/mod.rs
@@ -1,11 +1,61 @@
-use maud::{PreEscaped, html};
+use maud::{Markup, PreEscaped, Render, html};
 
+pub mod appbar;
+pub mod aspect;
+pub mod background;
+pub mod container;
+pub mod div;
+pub mod flex;
+pub mod header;
+pub mod image;
+pub mod link;
+pub mod padding;
+pub mod rounded;
 pub mod search;
+pub mod shadow;
+pub mod sized;
+pub mod text;
+pub mod width;
+
+// UI
+
+// Preludes
+
+// Basic Primitives
+pub mod basic {
+    pub use super::aspect::Aspect;
+    pub use super::background::Background;
+    pub use super::background::{Blue, Gray};
+    pub use super::container::Container;
+    pub use super::div::Div;
+    pub use super::flex::Flex;
+    pub use super::flex::Justify;
+    pub use super::header::Header;
+    pub use super::image::Image;
+    pub use super::link::Link;
+    pub use super::padding::Padding;
+    pub use super::rounded::Rounded;
+    pub use super::rounded::RoundedMedium;
+    pub use super::shadow::Shadow;
+    pub use super::sized::Sized;
+    pub use super::text::{Paragraph, Span, Text};
+    pub use super::width::FitWidth;
+}
+
+// Stacked Components
+pub mod extended {
+    pub use super::appbar::AppBar;
+}
 
 use crate::request::{RequestContext, StringResponse};
 
 use rocket::http::{ContentType, Status};
 
+#[allow(non_snake_case)]
+pub fn Nothing() -> PreEscaped<String> {
+    html! {}
+}
+
 /// Represents the HTML structure of a page shell, including the head, body class, and body content.
 ///
 /// This structure is used to construct the overall HTML structure of a page, making it easier to generate consistent HTML pages dynamically.
@@ -155,3 +205,42 @@ pub fn script(script: &str) -> PreEscaped<String> {
         };
     )
 }
+
+pub struct Row(PreEscaped<String>);
+
+impl Render for Row {
+    fn render(&self) -> maud::Markup {
+        html! {
+            div class="flex" { (self.0) }
+        }
+    }
+}
+
+// Grids
+
+// ListViews
+
+// ListTiles
+
+// Cards
+
+pub trait UIWidget: Render {
+    fn can_inherit(&self) -> bool;
+    fn render_with_class(&self, class: &str) -> Markup;
+}
+
+impl UIWidget for PreEscaped<String> {
+    fn can_inherit(&self) -> bool {
+        false
+    }
+
+    fn render_with_class(&self, _: &str) -> Markup {
+        self.render()
+    }
+}
+
+// TODO :
+// hover focus
+// responsive media
+// more elements
+// htmx builder trait?
diff --git a/src/ui/padding.rs b/src/ui/padding.rs
new file mode 100644
index 0000000..4962109
--- /dev/null
+++ b/src/ui/padding.rs
@@ -0,0 +1,83 @@
+use maud::{Markup, Render, html};
+
+use super::UIWidget;
+
+pub struct PaddingInfo {
+    pub right: Option<u32>,
+}
+
+#[allow(non_snake_case)]
+pub fn Padding<T: UIWidget + 'static>(inner: T) -> PaddingWidget {
+    PaddingWidget {
+        inner: Box::new(inner),
+        right: None,
+        y: None,
+        x: None,
+    }
+}
+
+pub struct PaddingWidget {
+    pub inner: Box<dyn UIWidget>,
+    pub right: Option<u32>,
+    pub y: Option<u32>,
+    pub x: Option<u32>,
+}
+
+impl PaddingWidget {
+    pub fn right(mut self, right: u32) -> Self {
+        self.right = Some(right);
+        self
+    }
+
+    pub fn y(mut self, y: u32) -> Self {
+        self.y = Some(y);
+        self
+    }
+
+    pub fn x(mut self, x: u32) -> Self {
+        self.x = Some(x);
+        self
+    }
+}
+
+impl Render for PaddingWidget {
+    fn render(&self) -> Markup {
+        self.render_with_class("")
+    }
+}
+
+impl UIWidget for PaddingWidget {
+    fn can_inherit(&self) -> bool {
+        true
+    }
+
+    fn render_with_class(&self, class: &str) -> Markup {
+        let mut our_class = Vec::new();
+
+        if let Some(r) = self.right {
+            our_class.push(format!("pr-{r}"));
+        }
+
+        if let Some(y) = self.y {
+            our_class.push(format!("py-{y}"));
+        }
+
+        if let Some(x) = self.x {
+            our_class.push(format!("px-{x}"));
+        }
+
+        let our_class = our_class.join(" ");
+
+        if self.inner.as_ref().can_inherit() {
+            self.inner
+                .as_ref()
+                .render_with_class(&format!("{our_class} {class}"))
+        } else {
+            html! {
+                div class=(format!("{our_class} {class}")) {
+                    (self.inner.as_ref())
+                }
+            }
+        }
+    }
+}
diff --git a/src/ui/rounded.rs b/src/ui/rounded.rs
new file mode 100644
index 0000000..1632274
--- /dev/null
+++ b/src/ui/rounded.rs
@@ -0,0 +1,41 @@
+use maud::{Markup, Render, html};
+
+use super::UIWidget;
+
+#[allow(non_snake_case)]
+pub fn Rounded<T: UIWidget + 'static>(inner: T) -> RoundedWidget {
+    RoundedWidget(Box::new(inner), "full".to_owned())
+}
+
+#[allow(non_snake_case)]
+pub fn RoundedMedium<T: UIWidget + 'static>(inner: T) -> RoundedWidget {
+    RoundedWidget(Box::new(inner), "md".to_owned())
+}
+
+pub struct RoundedWidget(Box<dyn UIWidget>, String);
+
+impl Render for RoundedWidget {
+    fn render(&self) -> Markup {
+        self.render_with_class("")
+    }
+}
+
+impl UIWidget for RoundedWidget {
+    fn can_inherit(&self) -> bool {
+        true
+    }
+
+    fn render_with_class(&self, class: &str) -> Markup {
+        if self.0.as_ref().can_inherit() {
+            self.0
+                .as_ref()
+                .render_with_class(&format!("rounded-{} {class}", self.1))
+        } else {
+            html! {
+                div class=(format!("rounded-{} {class}", self.1)) {
+                    (self.0.as_ref())
+                }
+            }
+        }
+    }
+}
diff --git a/src/page/search.rs b/src/ui/search.rs
similarity index 100%
rename from src/page/search.rs
rename to src/ui/search.rs
diff --git a/src/ui/shadow.rs b/src/ui/shadow.rs
new file mode 100644
index 0000000..5585d3b
--- /dev/null
+++ b/src/ui/shadow.rs
@@ -0,0 +1,37 @@
+use maud::{Markup, Render, html};
+
+use super::UIWidget;
+
+pub struct Shadow(Box<dyn UIWidget>, String);
+
+impl Shadow {
+    pub fn medium<T: UIWidget + 'static>(inner: T) -> Shadow {
+        Shadow(Box::new(inner), "md".to_owned())
+    }
+}
+
+impl Render for Shadow {
+    fn render(&self) -> Markup {
+        self.render_with_class("")
+    }
+}
+
+impl UIWidget for Shadow {
+    fn can_inherit(&self) -> bool {
+        true
+    }
+
+    fn render_with_class(&self, class: &str) -> Markup {
+        if self.0.as_ref().can_inherit() {
+            self.0
+                .as_ref()
+                .render_with_class(&format!("shadow-{} {class}", self.1))
+        } else {
+            html! {
+                div class=(format!("shadow-{} {class}", self.1)) {
+                    (self.0.as_ref())
+                }
+            }
+        }
+    }
+}
diff --git a/src/ui/sized.rs b/src/ui/sized.rs
new file mode 100644
index 0000000..dc473d6
--- /dev/null
+++ b/src/ui/sized.rs
@@ -0,0 +1,35 @@
+use super::UIWidget;
+use maud::{Markup, Render, html};
+
+#[allow(non_snake_case)]
+pub fn Sized<T: UIWidget + 'static>(height: u32, width: u32, inner: T) -> SizedWidget {
+    SizedWidget(Box::new(inner), height, width)
+}
+
+pub struct SizedWidget(Box<dyn UIWidget>, u32, u32);
+
+impl Render for SizedWidget {
+    fn render(&self) -> Markup {
+        self.render_with_class("")
+    }
+}
+
+impl UIWidget for SizedWidget {
+    fn can_inherit(&self) -> bool {
+        true
+    }
+
+    fn render_with_class(&self, class: &str) -> Markup {
+        if self.0.as_ref().can_inherit() {
+            self.0
+                .as_ref()
+                .render_with_class(&format!("h-{} w-{} {class}", self.1, self.2))
+        } else {
+            html! {
+                div class=(format!("h-{} w-{} {class}", self.1, self.2)) {
+                    (self.0.as_ref())
+                }
+            }
+        }
+    }
+}
diff --git a/src/ui/text.rs b/src/ui/text.rs
new file mode 100644
index 0000000..0a7c7f9
--- /dev/null
+++ b/src/ui/text.rs
@@ -0,0 +1,131 @@
+use super::UIWidget;
+use maud::{Markup, Render, html};
+
+#[allow(non_snake_case)]
+pub fn Text(txt: &str) -> TextWidget {
+    TextWidget {
+        inner: None,
+        txt: txt.to_string(),
+        font: String::new(),
+        color: String::new(),
+        size: String::new(),
+        span: false,
+    }
+}
+
+#[allow(non_snake_case)]
+pub fn Paragraph<T: UIWidget + 'static>(inner: T) -> TextWidget {
+    TextWidget {
+        inner: Some(Box::new(inner)),
+        font: String::new(),
+        color: String::new(),
+        txt: String::new(),
+        size: String::new(),
+        span: false,
+    }
+}
+
+#[allow(non_snake_case)]
+pub fn Span(txt: &str) -> TextWidget {
+    TextWidget {
+        inner: None,
+        txt: txt.to_string(),
+        font: String::new(),
+        color: String::new(),
+        size: String::new(),
+        span: true,
+    }
+}
+
+pub struct TextWidget {
+    inner: Option<Box<dyn UIWidget>>,
+    txt: String,
+    font: String,
+    color: String,
+    size: String,
+    span: bool,
+}
+
+impl TextWidget {
+    pub fn semibold(mut self) -> Self {
+        self.font = "font-semibold".to_owned();
+        self
+    }
+
+    pub fn bold(mut self) -> Self {
+        self.font = "font-bold".to_owned();
+        self
+    }
+
+    pub fn medium(mut self) -> Self {
+        self.font = "font-medium".to_owned();
+        self
+    }
+
+    pub fn _2xl(mut self) -> Self {
+        self.size = "text-2xl".to_owned();
+        self
+    }
+
+    pub fn xl(mut self) -> Self {
+        self.size = "text-xl".to_owned();
+        self
+    }
+
+    pub fn sm(mut self) -> Self {
+        self.size = "text-sm".to_owned();
+        self
+    }
+
+    pub fn gray(mut self, i: u32) -> Self {
+        self.color = format!("text-gray-{}", i);
+        self
+    }
+
+    pub fn slate(mut self, i: u32) -> Self {
+        self.color = format!("text-slate-{}", i);
+        self
+    }
+
+    pub fn white(mut self) -> Self {
+        self.color = "text-white".to_owned();
+        self
+    }
+}
+
+impl Render for TextWidget {
+    fn render(&self) -> Markup {
+        self.render_with_class("")
+    }
+}
+
+impl UIWidget for TextWidget {
+    fn can_inherit(&self) -> bool {
+        true
+    }
+
+    fn render_with_class(&self, class: &str) -> Markup {
+        let our_class = format!("{} {} {}", self.color, self.font, self.size);
+        if let Some(inner) = &self.inner {
+            if self.span {
+                html! {
+                    span class=(format!("{} {}", class, our_class)) { (inner) }
+                }
+            } else {
+                html! {
+                    p class=(format!("{} {}", class, our_class)) { (inner) }
+                }
+            }
+        } else {
+            if self.span {
+                html! {
+                    span class=(format!("{} {}", class, our_class)) { (self.txt) }
+                }
+            } else {
+                html! {
+                    p class=(format!("{} {}", class, our_class)) { (self.txt) }
+                }
+            }
+        }
+    }
+}
diff --git a/src/ui/width.rs b/src/ui/width.rs
new file mode 100644
index 0000000..9c7a390
--- /dev/null
+++ b/src/ui/width.rs
@@ -0,0 +1,36 @@
+use maud::{Markup, Render, html};
+
+use super::UIWidget;
+
+#[allow(non_snake_case)]
+pub fn FitWidth<T: UIWidget + 'static>(inner: T) -> FitWidthWidget {
+    FitWidthWidget(Box::new(inner))
+}
+
+pub struct FitWidthWidget(Box<dyn UIWidget>);
+
+impl Render for FitWidthWidget {
+    fn render(&self) -> Markup {
+        self.render_with_class("")
+    }
+}
+
+impl UIWidget for FitWidthWidget {
+    fn can_inherit(&self) -> bool {
+        true
+    }
+
+    fn render_with_class(&self, class: &str) -> Markup {
+        if self.0.as_ref().can_inherit() {
+            self.0
+                .as_ref()
+                .render_with_class(&format!("max-w-fit {class}"))
+        } else {
+            html! {
+                div class=(format!("max-w-fit {class}")) {
+                    (self.0.as_ref())
+                }
+            }
+        }
+    }
+}