diff --git a/demo/data/gui.yaml b/demo/data/gui.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..44aeecf791e0d7cc96de67097bf0be3d2c237a52
--- /dev/null
+++ b/demo/data/gui.yaml
@@ -0,0 +1,17 @@
+---
+- define: label
+  attrs:
+    value: """
+- define: button
+  children:
+    - template: label
+- define: textinput
+  children:
+    - template: label
+- define: checkbox
+  attrs:
+    state: False
+- define: radio
+  attrs:
+    state: False
+- define: frame
diff --git a/demo/demo/grid.cpp b/demo/demo/grid.cpp
index 9f373c5d12517cc50dcecd249a471b1e79a3de5b..b3c2760db6ca05d813ae4b330f028e29912a84bf 100644
--- a/demo/demo/grid.cpp
+++ b/demo/demo/grid.cpp
@@ -20,6 +20,7 @@
 
 #include "fggl/assets/loader.hpp"
 #include "fggl/entity/gridworld/zone.hpp"
+#include "fggl/gui/renderer/renderer.hpp"
 
 using namespace fggl::gfx::colours;
 
@@ -223,6 +224,30 @@ namespace demo {
 				}
 			}
 		}
+
+		// UI test
+		fggl::gui::model::Widget widget;
+		widget.set("position", fggl::math::vec2{200.0F, 100.F});
+		widget.set("size", fggl::math::vec2{500.0F, 300.F});
+		fggl::gui::model::attr_box_set(widget, "padding", 5.0F);
+		widget.set("colour", fggl::gfx::colours::BLANCHED_ALMOND);
+
+		fggl::gui::model::Widget handle;
+		handle.set("border::bottom",5.0F);
+		handle.set("position", fggl::math::vec2{0.0F, 0.0F});
+		handle.set("size", fggl::math::vec2{INFINITY, 50.0F});
+		handle.set("text", "hello, world!");
+		fggl::gui::model::attr_box_set(handle, "padding", 5.0F);
+		handle.set("colour", fggl::gfx::colours::ORANGE);
+		widget.addChild(handle);
+
+		fggl::gui::model::Widget content;
+		content.set("position", fggl::math::vec2{0.0F, 50.0F});
+		content.set("size", fggl::math::vec2{INFINITY, INFINITY});
+		content.set("colour", fggl::gfx::colours::BURLYWOOD);
+		widget.addChild(content);
+
+		fggl::gui::renderer::visit(widget, paint);
 	}
 
 
diff --git a/fggl/CMakeLists.txt b/fggl/CMakeLists.txt
index 9007ff0d49d0f172ea7070313964b20e5240723e..28461506a0a3726505142e16739aa3799fbc2445 100644
--- a/fggl/CMakeLists.txt
+++ b/fggl/CMakeLists.txt
@@ -56,12 +56,10 @@ target_sources(${PROJECT_NAME}
         input/input.cpp
         input/mouse.cpp
         input/camera_input.cpp
+)
 
-        gui/widget.cpp
-        gui/widgets.cpp
-        gui/containers.cpp
-        gui/fonts.cpp
-        )
+# GUI support
+add_subdirectory(gui)
 
 # yaml-cpp for configs and storage
 find_package(yaml-cpp)
@@ -70,9 +68,6 @@ target_link_libraries(fggl PUBLIC yaml-cpp)
 # model loading
 add_subdirectory(data/assimp)
 
-find_package(Freetype)
-target_link_libraries(${PROJECT_NAME} PUBLIC Freetype::Freetype)
-
 # Graphics backend
 add_subdirectory(gfx)
 add_subdirectory(audio)
diff --git a/fggl/gui/CMakeLists.txt b/fggl/gui/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..dc776db0f7946050b112c747864b63c78a30d022
--- /dev/null
+++ b/fggl/gui/CMakeLists.txt
@@ -0,0 +1,15 @@
+target_sources( ${PROJECT_NAME}
+    PRIVATE
+        widget.cpp
+        widgets.cpp
+        containers.cpp
+        fonts.cpp
+
+        model/parser.cpp
+        model/structure.cpp
+        renderer/renderer.cpp
+)
+
+find_package(Freetype)
+target_link_libraries(${PROJECT_NAME} PUBLIC Freetype::Freetype)
+
diff --git a/fggl/gui/model/parser.cpp b/fggl/gui/model/parser.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..ab42e05c7a4ffc7833d1dbfc422bca3e616998ba
--- /dev/null
+++ b/fggl/gui/model/parser.cpp
@@ -0,0 +1,59 @@
+/*
+ * This file is part of FGGL.
+ *
+ * FGGL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * FGGL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License along with FGGL.
+ * If not, see <https://www.gnu.org/licenses/>.
+ */
+
+//
+// Created by webpigeon on 11/03/23.
+//
+
+#include "fggl/gui/model/parser.hpp"
+#include <yaml-cpp/yaml.h>
+
+namespace fggl::gui::model {
+
+	Widget* YamlToWidgetTree(WidgetFactory& factory, const YAML::Node& config) {
+		Widget* root;
+		if ( config["template"] ) {
+			root = factory.build( config["template"].as<std::string>());
+		} else {
+			root = factory.buildEmpty();
+		}
+
+		// deal with attrs
+		for ( auto attr : config["attrs"] ) {
+			root->set(attr.first.as<std::string>(), attr.second.as<std::string>());
+		}
+
+		// deal with child nodes
+		for ( auto child : config["children"] ) {
+			Widget* childWidget = YamlToWidgetTree(factory, child);
+			root->addChild(*childWidget);
+		}
+
+		// are we a template definition?
+		if ( config["define"] ) {
+			factory.push( config["define"].as<std::string>(), *root );
+		}
+
+		return root;
+	}
+
+	inline Widget* parseFile(WidgetFactory& factory, const std::string& path) {
+		YAML::Node root = YAML::LoadFile(path);
+		if ( !root ){
+			return nullptr;
+		}
+		return YamlToWidgetTree(factory, root);
+	}
+
+}
\ No newline at end of file
diff --git a/fggl/gui/model/structure.cpp b/fggl/gui/model/structure.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..baf5d17f5ce5936b5b91c3b1dd2cefe66833afce
--- /dev/null
+++ b/fggl/gui/model/structure.cpp
@@ -0,0 +1,23 @@
+/*
+ * This file is part of FGGL.
+ *
+ * FGGL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * FGGL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License along with FGGL.
+ * If not, see <https://www.gnu.org/licenses/>.
+ */
+
+//
+// Created by webpigeon on 11/03/23.
+//
+
+#include "fggl/gui/model/structure.hpp"
+
+namespace fggl::gui::model {
+
+}
\ No newline at end of file
diff --git a/fggl/gui/renderer/renderer.cpp b/fggl/gui/renderer/renderer.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..8c6d98848e65da825db3aceeea1e192d2cd1b813
--- /dev/null
+++ b/fggl/gui/renderer/renderer.cpp
@@ -0,0 +1,107 @@
+/*
+ * This file is part of FGGL.
+ *
+ * FGGL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * FGGL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License along with FGGL.
+ * If not, see <https://www.gnu.org/licenses/>.
+ */
+
+//
+// Created by webpigeon on 11/03/23.
+//
+
+#include "fggl/gui/renderer/renderer.hpp"
+
+namespace fggl::gui::renderer {
+
+	void draw_box(gfx::Path2D &path, glm::vec2 topLeft, glm::vec2 bottomRight) {
+		path.moveTo({topLeft.x, topLeft.y});
+		path.pathTo({bottomRight.x, topLeft.y});
+		path.pathTo({bottomRight.x, bottomRight.y});
+		path.pathTo({topLeft.x, bottomRight.y});
+		path.pathTo({topLeft.x, topLeft.y});
+	}
+
+	void draw_border_patch(gfx::Paint& paint, Box& bounds, Box& size, math::vec3 colour) {
+		gfx::Path2D path({0,0});
+
+		path.colour(colour);
+		// draw edges
+		draw_box(path, {bounds.left + size.left, bounds.top}, {bounds.right - size.right, bounds.top + size.top} );
+		draw_box(path, {bounds.right - size.right, bounds.top + size.top}, {bounds.right, bounds.bottom - size.bottom} );
+		draw_box(path, {bounds.left + size.left, bounds.bottom - size.bottom}, {bounds.right - size.right, bounds.bottom} );
+		draw_box(path, {bounds.left, bounds.top + size.top}, {bounds.left + size.left, bounds.bottom - size.bottom} );
+
+		// draw-corners
+		draw_box(path, {bounds.left, bounds.top}, {bounds.left + size.left, bounds.top + size.top} );
+		draw_box(path, {bounds.right - size.right, bounds.top}, {bounds.right, bounds.top + size.top} );
+		draw_box(path, {bounds.left, bounds.bottom - size.bottom}, {bounds.left + size.left, bounds.bottom} );
+		draw_box(path, {bounds.right - size.right, bounds.bottom - size.bottom}, {bounds.right, bounds.bottom});
+		paint.fill(path);
+	}
+
+	void draw_border_solid(gfx::Paint& paint, Box& bounds, Box& size, math::vec3 colour) {
+		gfx::Path2D path({0,0});
+
+		path.colour(colour);
+		// draw edges
+		draw_box(path, {bounds.left, bounds.top}, {bounds.right, bounds.top + size.top} );
+		draw_box(path, {bounds.right - size.right, bounds.top + size.top}, {bounds.right, bounds.bottom - size.bottom} );
+		draw_box(path, {bounds.left, bounds.bottom - size.bottom}, {bounds.right, bounds.bottom} );
+		draw_box(path, {bounds.left, bounds.top + size.top}, {bounds.left + size.left, bounds.bottom - size.bottom} );
+
+		paint.fill(path);
+	}
+
+	void draw_background_solid(gfx::Paint& paint, Box& bounds, math::vec3 colour) {
+		gfx::Path2D path({0,0});
+		path.colour(colour);
+		draw_box(path, {bounds.left, bounds.top}, {bounds.right, bounds.bottom} );
+		paint.fill(path);
+	}
+
+	void visit(const model::Widget& root, gfx::Paint& paint, Box offset) {
+		// get border size
+		auto border = get_box(root, "border");
+
+		// calculate box bounds
+		auto pos = get_vec2(root, "position");
+		auto size = get_vec2(root, "size");
+
+		auto bounds = getBounds(pos, size);
+		bounds.top += offset.top;
+		bounds.left += offset.left;
+
+		// deal with right hand size bounds
+		bounds.right = std::min( size.x, offset.width() );
+		bounds.right += offset.left;
+
+		// deal with bottom bounds
+		bounds.bottom = std::min( size.y, offset.height() );
+		bounds.bottom += offset.top;
+
+		auto background = bounds.trim(border);
+
+		draw_background_solid(paint, background, get_vec3_rgb(root, "colour"));
+		draw_border_patch(paint, bounds, border, get_vec3_rgb(root, "border::colour"));
+
+		auto padding = get_box(root, "padding");
+		background = background.trim(padding);
+
+		if ( root.hasAttr("text") ) {
+			auto text = root.get<std::string>("text");
+			paint.text(text, {background.left, background.top + 15});
+		}
+
+		for (const auto& child : root) {
+			visit(child, paint, background);
+		}
+	}
+
+} // namespace fggl::gui:;renderer
\ No newline at end of file
diff --git a/include/fggl/gui/model/parser.hpp b/include/fggl/gui/model/parser.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..608aaa70ff4f82d16a25bf4c0c261d13e7860b3a
--- /dev/null
+++ b/include/fggl/gui/model/parser.hpp
@@ -0,0 +1,48 @@
+/*
+ * This file is part of FGGL.
+ *
+ * FGGL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * FGGL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License along with FGGL.
+ * If not, see <https://www.gnu.org/licenses/>.
+ */
+
+//
+// Created by webpigeon on 04/03/23.
+//
+
+#ifndef FGGL_GUI_MODEL_PARSER_HPP
+#define FGGL_GUI_MODEL_PARSER_HPP
+
+#include <fggl/gui/model/structure.hpp>
+
+namespace fggl::gui::model {
+
+	class WidgetFactory {
+		public:
+			inline Widget* build(std::string templateName) {
+				return new Widget(m_templates.at(templateName));
+			}
+
+			inline Widget* buildEmpty() {
+				return new Widget();
+			}
+
+			inline void push(std::string name, const Widget& definition) {
+				m_templates[name] = definition;
+			}
+
+		private:
+			std::map<std::string, Widget> m_templates;
+	};
+
+	Widget* parseFile(WidgetFactory& factory, const std::string& path);
+
+} // namespace fggl::gui::model
+
+#endif //FGGL_GUI_MODEL_PARSER_HPP
diff --git a/include/fggl/gui/model/structure.hpp b/include/fggl/gui/model/structure.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..a8cd119bbf348585e47089da3adb65c47c0ce825
--- /dev/null
+++ b/include/fggl/gui/model/structure.hpp
@@ -0,0 +1,128 @@
+/*
+ * This file is part of FGGL.
+ *
+ * FGGL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * FGGL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License along with FGGL.
+ * If not, see <https://www.gnu.org/licenses/>.
+ */
+
+//
+// Created by webpigeon on 04/03/23.
+//
+
+#ifndef FGGL_GUI_MODEL_STRUCTURE_HPP
+#define FGGL_GUI_MODEL_STRUCTURE_HPP
+
+#include <map>
+#include <variant>
+#include <vector>
+#include <string>
+
+#include <fggl/math/types.hpp>
+
+namespace fggl::gui::model {
+
+	using AttrKey = std::string;
+	using AttrValue = std::variant<bool, int, float, std::string, math::vec2, math::vec3>;
+
+	class Widget {
+		public:
+			using ChildItr = std::vector<Widget>::iterator;
+			using ChildItrConst = std::vector<Widget>::const_iterator;
+
+			inline bool isRoot() const {
+				return m_parent == nullptr;
+			}
+
+			inline bool isLeaf() const {
+				return m_children.empty();
+			}
+
+			inline bool hasAttr(const AttrKey& key) const {
+				return m_attrs.contains(key);
+			}
+
+			inline std::map<AttrKey, AttrValue> attrs() {
+				return m_attrs;
+			}
+
+			template<typename type>
+			inline type get(const AttrKey& key) const {
+				return std::get<type>(m_attrs.at(key));
+			}
+
+			template<typename type>
+			type get_or_default(const AttrKey& key) const {
+				auto itr = m_attrs.find(key);
+				if ( itr == m_attrs.end() ) {
+					return type();
+				} else {
+					return std::get<type>(itr->second);
+				}
+			}
+
+			template<typename type>
+			inline void set(const AttrKey& key, type value) {
+				m_attrs[key] = value;
+			}
+
+			inline void addChild(const Widget& element) {
+				auto childItr = m_children.insert( m_children.cend(), element );
+				childItr->m_parent = this;
+			}
+
+			inline ChildItr begin() {
+				return m_children.begin();
+			}
+
+			inline ChildItrConst begin() const noexcept {
+				return m_children.begin();
+			}
+
+			inline ChildItrConst cbegin() const noexcept {
+				return m_children.cbegin();
+			}
+
+			inline ChildItr end() {
+				return m_children.end();
+			}
+
+			inline ChildItrConst end() const {
+				return m_children.end();
+			}
+
+			inline ChildItrConst cend() const {
+				return m_children.cend();
+			}
+
+		private:
+			Widget *m_parent;
+			std::vector<Widget> m_children;
+			std::map< AttrKey, AttrValue > m_attrs;
+	};
+
+	void attr_box_set(Widget& widget, const AttrKey& key, auto top, auto right, auto bottom, auto left) {
+		widget.set(key + "::top", top);
+		widget.set(key + "::right", right);
+		widget.set(key + "::bottom", bottom);
+		widget.set(key + "::left", left);
+	}
+
+	inline void attr_box_set(Widget& widget, const AttrKey& key, auto vert, auto horz) {
+		attr_box_set(widget, key, vert, horz, vert, horz);
+	}
+
+	inline void attr_box_set(Widget& widget, const AttrKey& key, auto value) {
+		attr_box_set(widget, key, value, value, value, value);
+	}
+
+
+} // namespace fggl::gui::model
+
+#endif //FGGL_GUI_MODEL_STRUCTURE_HPP
diff --git a/include/fggl/gui/renderer/renderer.hpp b/include/fggl/gui/renderer/renderer.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..c90d8034c9ac6af88c4f1fab4ebe71a098adb4d8
--- /dev/null
+++ b/include/fggl/gui/renderer/renderer.hpp
@@ -0,0 +1,104 @@
+/*
+ * This file is part of FGGL.
+ *
+ * FGGL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * FGGL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License along with FGGL.
+ * If not, see <https://www.gnu.org/licenses/>.
+ */
+
+//
+// Created by webpigeon on 04/03/23.
+//
+
+#ifndef FGGL_GUI_RENDERER_RENDERER_HPP
+#define FGGL_GUI_RENDERER_RENDERER_HPP
+
+#include <fggl/gui/model/structure.hpp>
+#include <fggl/gfx/paint.hpp>
+
+namespace fggl::gui::renderer {
+
+	struct Box {
+		float top;
+		float right;
+		float bottom;
+		float left;
+
+		inline Box trim(Box border) const {
+			return {
+				top + border.top,
+				right - border.right,
+				bottom - border.bottom,
+				left + border.left
+			};
+		}
+
+		inline Box expand(Box border) const {
+			return {
+				top - border.top,
+				right + border.right,
+				bottom + border.bottom,
+				left - border.left
+			};
+		}
+
+		inline float width() const {
+			return right - left;
+		}
+
+		inline float height() const {
+			return bottom - top;
+		}
+	};
+
+	inline math::vec2 get_vec2(const model::Widget& root, const std::string& name) {
+		if ( !root.hasAttr(name) ) {
+			return {};
+		}
+		return root.get<math::vec2>(name);
+	}
+
+	inline math::vec3 get_vec3_rgb(const model::Widget& root, const std::string& name) {
+		if ( !root.hasAttr(name) ) {
+			return {};
+		}
+		return root.get<math::vec3>(name);
+	}
+
+	inline Box get_box(const model::Widget& root, const std::string& name) {
+		return {
+			root.get_or_default<float>(name + "::top"),
+			root.get_or_default<float>(name + "::right"),
+			root.get_or_default<float>(name + "::bottom"),
+			root.get_or_default<float>(name + "::left")
+		};
+	}
+
+	inline Box getBounds(math::vec2 pos, math::vec2 size) {
+		return {
+			pos.y,
+			pos.x + size.x,
+			pos.y + size.y,
+			pos.x
+		};
+	}
+
+	void draw_box(gfx::Path2D &path, glm::vec2 topLeft, glm::vec2 bottomRight);
+
+	void draw_border_patch(gfx::Paint& paint, Box& bounds, Box& size, math::vec3 colour);
+
+	void draw_border_solid(gfx::Paint& paint, Box& bounds, Box& size, math::vec3 colour);
+
+	void draw_background_solid(gfx::Paint& paint, Box& bounds, math::vec3 colour);
+
+	void visit(const model::Widget& root, gfx::Paint& paint, Box offset = {0.0F, 1024.0F, 768.0F, 0.0F});
+
+} // namespace fggl::gui::renderer
+
+#endif //FGGL_GUI_RENDERER_RENDERER_HPP