diff --git a/bevy_kayak_ui/src/bevy_context.rs b/bevy_kayak_ui/src/bevy_context.rs
index 6708d8e761dffcdf631338eef44594b5800483d2..5eb9cfe732618bfef4450c4c14a44fb4a3a7e7a8 100644
--- a/bevy_kayak_ui/src/bevy_context.rs
+++ b/bevy_kayak_ui/src/bevy_context.rs
@@ -6,11 +6,11 @@ use kayak_core::{
     styles::{Style, StyleProp, Units},
 };
 
-pub struct BevyContext<'a> {
-    pub kayak_context: Arc<RwLock<KayakContext<'a>>>,
+pub struct BevyContext {
+    pub kayak_context: Arc<RwLock<KayakContext>>,
 }
 
-impl<'a> BevyContext<'a> {
+impl BevyContext {
     pub fn new<F: Fn(&mut Style, &mut KayakContext)>(width: f32, height: f32, f: F) -> Self {
         let mut app_styles = Style {
             render_command: StyleProp::Value(RenderCommand::Window),
diff --git a/bevy_kayak_ui/src/lib.rs b/bevy_kayak_ui/src/lib.rs
index ced795308fcbea7d33ee85587415dca11704289c..1f1d45ff7c38e3add19967a1d7e743095733ed21 100644
--- a/bevy_kayak_ui/src/lib.rs
+++ b/bevy_kayak_ui/src/lib.rs
@@ -1,9 +1,7 @@
 use bevy::{
     input::{mouse::MouseButtonInput, ElementState},
     math::Vec2,
-    prelude::{
-        EventReader, IntoExclusiveSystem, IntoSystem, MouseButton, Mut, Plugin, Res, ResMut, World,
-    },
+    prelude::{EventReader, IntoExclusiveSystem, MouseButton, Plugin, Res, World},
     render2::color::Color,
     window::{CursorMoved, Windows},
 };
@@ -14,7 +12,8 @@ mod render;
 
 pub use bevy_context::BevyContext;
 pub use camera::*;
-use kayak_core::{context::GlobalState, InputEvent};
+use kayak_core::InputEvent;
+pub use render::unified::image::ImageManager;
 
 #[derive(Default)]
 pub struct BevyKayakUIPlugin;
@@ -23,6 +22,7 @@ impl Plugin for BevyKayakUIPlugin {
     fn build(&self, app: &mut bevy::prelude::App) {
         app.add_plugin(render::BevyKayakUIRenderPlugin)
             .add_plugin(camera::KayakUICameraPlugin)
+            .add_system(process_events)
             .add_system(update.exclusive_system());
     }
 }
@@ -31,61 +31,50 @@ pub(crate) fn to_bevy_color(color: &kayak_core::color::Color) -> Color {
     Color::rgba(color.r, color.g, color.b, color.a)
 }
 
-pub struct WrappedWorld<'a> {
-    world: &'a mut World,
-}
-
-impl<'a> GlobalState for WrappedWorld<'a> {}
-
 pub fn update(world: &mut World) {
-    let window_size = {
-        let windows = world.get_resource::<Windows>().unwrap();
-        if let Some(window) = windows.get_primary() {
-            Vec2::new(window.width(), window.height())
-        } else {
-            panic!("Couldn't find primary window!");
-        }
-    };
-
-    let bevy_context = world.remove_resource::<BevyContext<'static>>().unwrap();
+    let bevy_context = world.remove_resource::<BevyContext>().unwrap();
     if let Ok(mut context) = bevy_context.kayak_context.write() {
-        let mut wrapped_world = WrappedWorld { world };
-        context.render(&mut wrapped_world);
+        context.set_global_state(std::mem::take(world));
+        context.render();
+        *world = context.take_global_state::<World>().unwrap()
     }
 
+    world.insert_resource(bevy_context);
+}
+
+pub fn process_events(
+    bevy_context: Res<BevyContext>,
+    windows: Res<Windows>,
+    mut cursor_moved_events: EventReader<CursorMoved>,
+    mut mouse_button_input_events: EventReader<MouseButtonInput>,
+) {
+    let window_size = if let Some(window) = windows.get_primary() {
+        Vec2::new(window.width(), window.height())
+    } else {
+        panic!("Couldn't find primary window!");
+    };
+
     if let Ok(mut context) = bevy_context.kayak_context.write() {
         let mut input_events = Vec::new();
 
-        {
-            let mut cursor_moved_events = world
-                .get_resource_mut::<EventReader<CursorMoved>>()
-                .unwrap();
-            for event in cursor_moved_events.iter() {
-                input_events.push(InputEvent::MouseMoved((
-                    event.position.x as f32,
-                    window_size.y - event.position.y as f32,
-                )));
-            }
+        for event in cursor_moved_events.iter() {
+            input_events.push(InputEvent::MouseMoved((
+                event.position.x as f32,
+                window_size.y - event.position.y as f32,
+            )));
         }
 
-        {
-            let mut mouse_button_input_events = world
-                .get_resource_mut::<EventReader<MouseButtonInput>>()
-                .unwrap();
-            for event in mouse_button_input_events.iter() {
-                match event.button {
-                    MouseButton::Left => {
-                        if event.state == ElementState::Pressed {
-                            input_events.push(InputEvent::MouseLeftClick);
-                        }
+        for event in mouse_button_input_events.iter() {
+            match event.button {
+                MouseButton::Left => {
+                    if event.state == ElementState::Pressed {
+                        input_events.push(InputEvent::MouseLeftClick);
                     }
-                    _ => {}
                 }
+                _ => {}
             }
         }
 
         context.process_events(input_events);
     }
-
-    world.insert_resource(bevy_context);
 }
diff --git a/bevy_kayak_ui/src/render/mod.rs b/bevy_kayak_ui/src/render/mod.rs
index 6b41bf0457c2946305253858b7e5199855b0cba4..970ea9c859b141e90548d502c16562a44cb9a9d0 100644
--- a/bevy_kayak_ui/src/render/mod.rs
+++ b/bevy_kayak_ui/src/render/mod.rs
@@ -19,7 +19,7 @@ use self::ui_pass::TransparentUI;
 
 mod ui_pass;
 mod ui_pass_driver;
-mod unified;
+pub mod unified;
 
 pub mod node {
     pub const UI_PASS_DEPENDENCIES: &str = "ui_pass_dependencies";
diff --git a/bevy_kayak_ui/src/render/unified/font/extract.rs b/bevy_kayak_ui/src/render/unified/font/extract.rs
index 585f124a89a3fc9f9bb2299df7e95597e5980908..5948efc52b5af61e915b904c7be8bf431dfcf9ca 100644
--- a/bevy_kayak_ui/src/render/unified/font/extract.rs
+++ b/bevy_kayak_ui/src/render/unified/font/extract.rs
@@ -14,7 +14,7 @@ use super::{font::KayakFont, font_mapping::FontMapping};
 
 pub fn extract_texts(
     mut commands: Commands,
-    context: Res<BevyContext<'static>>,
+    context: Res<BevyContext>,
     mut fonts: ResMut<Assets<KayakFont>>,
     font_mapping: Res<FontMapping>,
 ) {
@@ -137,6 +137,7 @@ pub fn extract_texts(
                         quad_type: UIQuadType::Text,
                         type_index: 0,
                         border_radius: (0.0, 0.0, 0.0, 0.0),
+                        image: None,
                     },
                 });
 
diff --git a/bevy_kayak_ui/src/render/unified/font/font_texture_cache.rs b/bevy_kayak_ui/src/render/unified/font/font_texture_cache.rs
index f45898b206eac724845a3ff152defc20f0c621b3..572627267a4a4011ecd6518ec43fb8bb0906daa7 100644
--- a/bevy_kayak_ui/src/render/unified/font/font_texture_cache.rs
+++ b/bevy_kayak_ui/src/render/unified/font/font_texture_cache.rs
@@ -111,7 +111,7 @@ impl FontTextureCache {
                                 resource: BindingResource::Sampler(&gpu_image.sampler),
                             },
                         ],
-                        layout: &pipeline.image_layout,
+                        layout: &pipeline.font_image_layout,
                     });
 
                     self.bind_groups
@@ -333,7 +333,7 @@ impl FontTextureCache {
                     resource: BindingResource::Sampler(&gpu_image.sampler),
                 },
             ],
-            layout: &pipeline.image_layout,
+            layout: &pipeline.font_image_layout,
         });
 
         bind_groups.insert(font_handle.clone_weak(), binding);
diff --git a/bevy_kayak_ui/src/render/unified/image/extract.rs b/bevy_kayak_ui/src/render/unified/image/extract.rs
new file mode 100644
index 0000000000000000000000000000000000000000..fee53b688ede10e56af934611176b295eb0c97c5
--- /dev/null
+++ b/bevy_kayak_ui/src/render/unified/image/extract.rs
@@ -0,0 +1,58 @@
+use bevy::{
+    math::Vec2,
+    prelude::{Commands, Res},
+    render2::color::Color,
+    sprite2::Rect,
+};
+use kayak_core::render_primitive::RenderPrimitive;
+
+use crate::{
+    render::unified::pipeline::{ExtractQuadBundle, ExtractedQuad, UIQuadType},
+    BevyContext, ImageManager,
+};
+
+pub fn extract_images(
+    mut commands: Commands,
+    context: Res<BevyContext>,
+    image_manager: Res<ImageManager>,
+) {
+    let render_commands = if let Ok(context) = context.kayak_context.read() {
+        context.widget_manager.build_render_primitives()
+    } else {
+        vec![]
+    };
+
+    let image_commands: Vec<&RenderPrimitive> = render_commands
+        .iter()
+        .filter(|command| matches!(command, RenderPrimitive::Image { .. }))
+        .collect::<Vec<_>>();
+
+    let mut extracted_quads = Vec::new();
+    for render_primitive in image_commands {
+        let (layout, handle) = match render_primitive {
+            RenderPrimitive::Image { layout, handle } => (layout, handle),
+            _ => panic!(""),
+        };
+
+        extracted_quads.push(ExtractQuadBundle {
+            extracted_quad: ExtractedQuad {
+                rect: Rect {
+                    min: Vec2::new(layout.posx, layout.posy),
+                    max: Vec2::new(layout.posx + layout.width, layout.posy + layout.height),
+                },
+                color: Color::WHITE,
+                vertex_index: 0,
+                char_id: 0,
+                z_index: layout.z_index,
+                font_handle: None,
+                quad_type: UIQuadType::Image,
+                type_index: 0,
+                border_radius: (0.0, 0.0, 0.0, 0.0),
+                image: image_manager
+                    .get_handle(handle)
+                    .and_then(|a| Some(a.clone_weak())),
+            },
+        });
+    }
+    commands.spawn_batch(extracted_quads);
+}
diff --git a/bevy_kayak_ui/src/render/unified/image/image_manager.rs b/bevy_kayak_ui/src/render/unified/image/image_manager.rs
new file mode 100644
index 0000000000000000000000000000000000000000..b92b08e5ce9a0db06b16b2399cbd93aefce1c4db
--- /dev/null
+++ b/bevy_kayak_ui/src/render/unified/image/image_manager.rs
@@ -0,0 +1,34 @@
+use bevy::{prelude::Handle, render2::texture::Image, utils::HashMap};
+
+#[derive(Debug, Clone)]
+pub struct ImageManager {
+    count: u16,
+    mapping: HashMap<u16, Handle<Image>>,
+    reverse_mapping: HashMap<Handle<Image>, u16>,
+}
+
+impl ImageManager {
+    pub fn new() -> Self {
+        Self {
+            count: 0,
+            mapping: HashMap::default(),
+            reverse_mapping: HashMap::default(),
+        }
+    }
+
+    pub fn get(&mut self, image_handle: &Handle<Image>) -> u16 {
+        if let Some(id) = self.reverse_mapping.get(image_handle) {
+            return *id;
+        } else {
+            let id = self.count;
+            self.count += 1;
+            self.mapping.insert(id, image_handle.clone_weak());
+            self.reverse_mapping.insert(image_handle.clone_weak(), id);
+            return id;
+        }
+    }
+
+    pub fn get_handle(&self, id: &u16) -> Option<&Handle<Image>> {
+        self.mapping.get(id)
+    }
+}
diff --git a/bevy_kayak_ui/src/render/unified/image/mod.rs b/bevy_kayak_ui/src/render/unified/image/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..549b0970c0884f0c0d46223f54a25a8a2ef87cbd
--- /dev/null
+++ b/bevy_kayak_ui/src/render/unified/image/mod.rs
@@ -0,0 +1,19 @@
+use bevy::{
+    prelude::Plugin,
+    render2::{RenderApp, RenderStage},
+};
+
+mod extract;
+mod image_manager;
+pub use image_manager::ImageManager;
+
+pub struct ImageRendererPlugin;
+
+impl Plugin for ImageRendererPlugin {
+    fn build(&self, app: &mut bevy::prelude::App) {
+        app.insert_resource(ImageManager::new());
+
+        let render_app = app.sub_app(RenderApp);
+        render_app.add_system_to_stage(RenderStage::Extract, extract::extract_images);
+    }
+}
diff --git a/bevy_kayak_ui/src/render/unified/mod.rs b/bevy_kayak_ui/src/render/unified/mod.rs
index 37aeb58243aa976470811d76a83d949f8754ca34..c6ce51ce23c0d449835082a6f5eab7ec51138405 100644
--- a/bevy_kayak_ui/src/render/unified/mod.rs
+++ b/bevy_kayak_ui/src/render/unified/mod.rs
@@ -9,7 +9,10 @@ use crate::render::{
     unified::pipeline::{DrawUI, QuadMeta, UnifiedPipeline},
 };
 
+use self::pipeline::ImageBindGroups;
+
 pub mod font;
+pub mod image;
 mod pipeline;
 mod quad;
 
@@ -26,6 +29,7 @@ impl Plugin for UnifiedRenderPlugin {
 
         let render_app = app.sub_app(RenderApp);
         render_app
+            .init_resource::<ImageBindGroups>()
             .init_resource::<UnifiedPipeline>()
             .init_resource::<QuadMeta>()
             .add_system_to_stage(RenderStage::Prepare, pipeline::prepare_quads)
@@ -41,6 +45,7 @@ impl Plugin for UnifiedRenderPlugin {
             .add(draw_quad);
 
         app.add_plugin(font::TextRendererPlugin)
-            .add_plugin(quad::QuadRendererPlugin);
+            .add_plugin(quad::QuadRendererPlugin)
+            .add_plugin(image::ImageRendererPlugin);
     }
 }
diff --git a/bevy_kayak_ui/src/render/unified/pipeline.rs b/bevy_kayak_ui/src/render/unified/pipeline.rs
index f110ca4fd630842b5def4579c9e89c7c55378df9..483983c32551ca37914d26473d9c1e3b63fede5b 100644
--- a/bevy_kayak_ui/src/render/unified/pipeline.rs
+++ b/bevy_kayak_ui/src/render/unified/pipeline.rs
@@ -8,22 +8,26 @@ use bevy::{
     prelude::{Bundle, Component, Entity, FromWorld, Handle, Query, Res, ResMut, World},
     render2::{
         color::Color,
+        render_asset::RenderAssets,
         render_phase::{Draw, DrawFunctions, RenderPhase, TrackedRenderPass},
         render_resource::{
             BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout,
-            BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingType, BlendComponent,
-            BlendFactor, BlendOperation, BlendState, BufferBindingType, BufferSize, BufferUsages,
-            BufferVec, CachedPipelineId, ColorTargetState, ColorWrites, DynamicUniformVec,
-            FragmentState, FrontFace, MultisampleState, PolygonMode, PrimitiveState,
-            PrimitiveTopology, RenderPipelineCache, RenderPipelineDescriptor, Shader, ShaderStages,
-            TextureFormat, TextureSampleType, TextureViewDimension, VertexAttribute,
-            VertexBufferLayout, VertexFormat, VertexState, VertexStepMode,
+            BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType,
+            BlendComponent, BlendFactor, BlendOperation, BlendState, BufferBindingType, BufferSize,
+            BufferUsages, BufferVec, CachedPipelineId, ColorTargetState, ColorWrites,
+            DynamicUniformVec, Extent3d, FragmentState, FrontFace, MultisampleState, PolygonMode,
+            PrimitiveState, PrimitiveTopology, RenderPipelineCache, RenderPipelineDescriptor,
+            SamplerDescriptor, Shader, ShaderStages, TextureDescriptor, TextureDimension,
+            TextureFormat, TextureSampleType, TextureUsages, TextureViewDescriptor,
+            TextureViewDimension, VertexAttribute, VertexBufferLayout, VertexFormat, VertexState,
+            VertexStepMode,
         },
         renderer::{RenderDevice, RenderQueue},
-        texture::{BevyDefault, GpuImage},
+        texture::{BevyDefault, GpuImage, Image},
         view::{ViewUniformOffset, ViewUniforms},
     },
     sprite2::Rect,
+    utils::HashMap,
 };
 use bytemuck::{Pod, Zeroable};
 use crevice::std140::AsStd140;
@@ -35,9 +39,11 @@ use crate::render::ui_pass::TransparentUI;
 pub struct UnifiedPipeline {
     view_layout: BindGroupLayout,
     types_layout: BindGroupLayout,
-    pub(crate) image_layout: BindGroupLayout,
+    pub(crate) font_image_layout: BindGroupLayout,
+    image_layout: BindGroupLayout,
     pipeline: CachedPipelineId,
     empty_font_texture: (GpuImage, BindGroup),
+    default_image: (GpuImage, BindGroup),
 }
 
 const QUAD_VERTEX_POSITIONS: &[Vec3] = &[
@@ -87,6 +93,33 @@ impl FromWorld for UnifiedPipeline {
             label: Some("ui_types_layout"),
         });
 
+        // Used by fonts
+        let font_image_layout =
+            render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
+                entries: &[
+                    BindGroupLayoutEntry {
+                        binding: 0,
+                        visibility: ShaderStages::FRAGMENT,
+                        ty: BindingType::Texture {
+                            multisampled: false,
+                            sample_type: TextureSampleType::Float { filterable: false },
+                            view_dimension: TextureViewDimension::D2Array,
+                        },
+                        count: None,
+                    },
+                    BindGroupLayoutEntry {
+                        binding: 1,
+                        visibility: ShaderStages::FRAGMENT,
+                        ty: BindingType::Sampler {
+                            comparison: false,
+                            filtering: true,
+                        },
+                        count: None,
+                    },
+                ],
+                label: Some("text_image_layout"),
+            });
+
         let image_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
             entries: &[
                 BindGroupLayoutEntry {
@@ -95,7 +128,7 @@ impl FromWorld for UnifiedPipeline {
                     ty: BindingType::Texture {
                         multisampled: false,
                         sample_type: TextureSampleType::Float { filterable: false },
-                        view_dimension: TextureViewDimension::D2Array,
+                        view_dimension: TextureViewDimension::D2,
                     },
                     count: None,
                 },
@@ -109,7 +142,7 @@ impl FromWorld for UnifiedPipeline {
                     count: None,
                 },
             ],
-            label: Some("text_image_layout"),
+            label: Some("image_layout"),
         });
 
         let vertex_buffer_layout = VertexBufferLayout {
@@ -139,7 +172,7 @@ impl FromWorld for UnifiedPipeline {
             ],
         };
 
-        let empty_font_texture = FontTextureCache::get_empty(&render_device, &image_layout);
+        let empty_font_texture = FontTextureCache::get_empty(&render_device, &font_image_layout);
 
         let pipeline_desc = RenderPipelineDescriptor {
             vertex: VertexState {
@@ -171,8 +204,9 @@ impl FromWorld for UnifiedPipeline {
             }),
             layout: Some(vec![
                 view_layout.clone(),
-                image_layout.clone(),
+                font_image_layout.clone(),
                 types_layout.clone(),
+                image_layout.clone(),
             ]),
             primitive: PrimitiveState {
                 front_face: FrontFace::Ccw,
@@ -189,15 +223,68 @@ impl FromWorld for UnifiedPipeline {
                 mask: !0,
                 alpha_to_coverage_enabled: false,
             },
-            label: Some("quad_pipeline".into()),
+            label: Some("unified_pipeline".into()),
+        };
+
+        let texture_descriptor = TextureDescriptor {
+            label: Some("font_texture_array"),
+            size: Extent3d {
+                width: 1,
+                height: 1,
+                depth_or_array_layers: 1,
+            },
+            mip_level_count: 1,
+            sample_count: 1,
+            dimension: TextureDimension::D2,
+            format: TextureFormat::Rgba8UnormSrgb,
+            usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
+        };
+
+        let sampler_descriptor = SamplerDescriptor::default();
+
+        let texture = render_device.create_texture(&texture_descriptor);
+        let sampler = render_device.create_sampler(&sampler_descriptor);
+
+        let texture_view = texture.create_view(&TextureViewDescriptor {
+            label: Some("font_texture_array_view"),
+            format: None,
+            dimension: Some(TextureViewDimension::D2),
+            aspect: bevy::render2::render_resource::TextureAspect::All,
+            base_mip_level: 0,
+            base_array_layer: 0,
+            mip_level_count: None,
+            array_layer_count: None,
+        });
+
+        let image = GpuImage {
+            texture,
+            sampler,
+            texture_view,
         };
 
+        let binding = render_device.create_bind_group(&BindGroupDescriptor {
+            label: Some("text_image_bind_group"),
+            entries: &[
+                BindGroupEntry {
+                    binding: 0,
+                    resource: BindingResource::TextureView(&image.texture_view),
+                },
+                BindGroupEntry {
+                    binding: 1,
+                    resource: BindingResource::Sampler(&image.sampler),
+                },
+            ],
+            layout: &image_layout,
+        });
+
         UnifiedPipeline {
             pipeline: pipeline_cache.queue(pipeline_desc),
             view_layout,
-            image_layout,
+            font_image_layout,
             empty_font_texture,
             types_layout,
+            image_layout,
+            default_image: (image, binding),
         }
     }
 }
@@ -211,6 +298,7 @@ pub struct ExtractQuadBundle {
 pub enum UIQuadType {
     Quad,
     Text,
+    Image,
 }
 
 #[derive(Component)]
@@ -224,6 +312,7 @@ pub struct ExtractedQuad {
     pub quad_type: UIQuadType,
     pub type_index: u32,
     pub border_radius: (f32, f32, f32, f32),
+    pub image: Option<Handle<Image>>,
 }
 
 #[repr(C)]
@@ -259,6 +348,11 @@ impl Default for QuadMeta {
     }
 }
 
+#[derive(Default)]
+pub struct ImageBindGroups {
+    values: HashMap<Handle<Image>, BindGroup>,
+}
+
 pub fn prepare_quads(
     render_device: Res<RenderDevice>,
     render_queue: Res<RenderQueue>,
@@ -266,7 +360,7 @@ pub fn prepare_quads(
     mut extracted_quads: Query<&mut ExtractedQuad>,
 ) {
     let extracted_sprite_len = extracted_quads.iter_mut().len();
-    // dont create buffers when there are no quads
+    // don't create buffers when there are no quads
     if extracted_sprite_len == 0 {
         return;
     }
@@ -275,6 +369,7 @@ pub fn prepare_quads(
     sprite_meta.types_buffer.reserve(2, &render_device);
     let quad_type_offset = sprite_meta.types_buffer.push(QuadType { t: 0 });
     let text_type_offset = sprite_meta.types_buffer.push(QuadType { t: 1 });
+    let image_type_offset = sprite_meta.types_buffer.push(QuadType { t: 2 });
     sprite_meta
         .types_buffer
         .write_buffer(&render_device, &render_queue);
@@ -292,6 +387,7 @@ pub fn prepare_quads(
         match extracted_sprite.quad_type {
             UIQuadType::Quad => extracted_sprite.type_index = quad_type_offset,
             UIQuadType::Text => extracted_sprite.type_index = text_type_offset,
+            UIQuadType::Image => extracted_sprite.type_index = image_type_offset,
         };
 
         let bottom_left = Vec4::new(
@@ -362,6 +458,9 @@ pub fn queue_quads(
     quad_pipeline: Res<UnifiedPipeline>,
     mut extracted_sprites: Query<(Entity, &ExtractedQuad)>,
     mut views: Query<&mut RenderPhase<TransparentUI>>,
+    mut image_bind_groups: ResMut<ImageBindGroups>,
+    unified_pipeline: Res<UnifiedPipeline>,
+    gpu_images: Res<RenderAssets<Image>>,
 ) {
     if let Some(type_binding) = sprite_meta.types_buffer.binding() {
         sprite_meta.types_bind_group =
@@ -384,9 +483,34 @@ pub fn queue_quads(
             label: Some("quad_view_bind_group"),
             layout: &quad_pipeline.view_layout,
         }));
+
         let draw_quad = draw_functions.read().get_id::<DrawUI>().unwrap();
         for mut transparent_phase in views.iter_mut() {
             for (entity, quad) in extracted_sprites.iter_mut() {
+                if let Some(image_handle) = quad.image.as_ref() {
+                    image_bind_groups
+                        .values
+                        .entry(image_handle.clone_weak())
+                        .or_insert_with(|| {
+                            let gpu_image = gpu_images.get(&image_handle).unwrap();
+                            render_device.create_bind_group(&BindGroupDescriptor {
+                                entries: &[
+                                    BindGroupEntry {
+                                        binding: 0,
+                                        resource: BindingResource::TextureView(
+                                            &gpu_image.texture_view,
+                                        ),
+                                    },
+                                    BindGroupEntry {
+                                        binding: 1,
+                                        resource: BindingResource::Sampler(&gpu_image.sampler),
+                                    },
+                                ],
+                                label: Some("ui_image_bind_group"),
+                                layout: &unified_pipeline.image_layout,
+                            })
+                        });
+                }
                 transparent_phase.add(TransparentUI {
                     draw_function: draw_quad,
                     pipeline: quad_pipeline.pipeline,
@@ -404,6 +528,7 @@ pub struct DrawUI {
         SRes<UnifiedPipeline>,
         SRes<RenderPipelineCache>,
         SRes<FontTextureCache>,
+        SRes<ImageBindGroups>,
         SQuery<Read<ViewUniformOffset>>,
         SQuery<Read<ExtractedQuad>>,
     )>,
@@ -425,8 +550,16 @@ impl Draw<TransparentUI> for DrawUI {
         view: Entity,
         item: &TransparentUI,
     ) {
-        let (quad_meta, unified_pipeline, pipelines, font_texture_cache, views, quads) =
-            self.params.get(world);
+        let (
+            quad_meta,
+            unified_pipeline,
+            pipelines,
+            font_texture_cache,
+            image_bind_groups,
+            views,
+            quads,
+        ) = self.params.get(world);
+
         let view_uniform = views.get(view).unwrap();
         let quad_meta = quad_meta.into_inner();
         let extracted_quad = quads.get(item.entity).unwrap();
@@ -445,20 +578,31 @@ impl Draw<TransparentUI> for DrawUI {
                 &[extracted_quad.type_index],
             );
 
+            let unified_pipeline = unified_pipeline.into_inner();
             if let Some(font_handle) = extracted_quad.font_handle.as_ref() {
                 if let Some(image_bindings) =
                     font_texture_cache.into_inner().bind_groups.get(font_handle)
                 {
                     pass.set_bind_group(1, image_bindings, &[]);
                 } else {
-                    pass.set_bind_group(
-                        1,
-                        &unified_pipeline.into_inner().empty_font_texture.1,
-                        &[],
-                    );
+                    pass.set_bind_group(1, &unified_pipeline.empty_font_texture.1, &[]);
                 }
             } else {
-                pass.set_bind_group(1, &unified_pipeline.into_inner().empty_font_texture.1, &[]);
+                pass.set_bind_group(1, &unified_pipeline.empty_font_texture.1, &[]);
+            }
+
+            if let Some(image_handle) = extracted_quad.image.as_ref() {
+                pass.set_bind_group(
+                    3,
+                    &image_bind_groups
+                        .into_inner()
+                        .values
+                        .get(image_handle)
+                        .unwrap(),
+                    &[],
+                );
+            } else {
+                pass.set_bind_group(3, &unified_pipeline.default_image.1, &[]);
             }
 
             pass.draw(
diff --git a/bevy_kayak_ui/src/render/unified/quad/extract.rs b/bevy_kayak_ui/src/render/unified/quad/extract.rs
index 1290ae56286f9dbb7482a426ef7a6bf21aeddcf3..b81a7d0d9e18bec63f715dc0d4b210e7b61487b9 100644
--- a/bevy_kayak_ui/src/render/unified/quad/extract.rs
+++ b/bevy_kayak_ui/src/render/unified/quad/extract.rs
@@ -10,7 +10,7 @@ use crate::{
     to_bevy_color, BevyContext,
 };
 
-pub fn extract_quads(mut commands: Commands, context: Res<BevyContext<'static>>) {
+pub fn extract_quads(mut commands: Commands, context: Res<BevyContext>) {
     let render_commands = if let Ok(context) = context.kayak_context.read() {
         context.widget_manager.build_render_primitives()
     } else {
@@ -47,6 +47,7 @@ pub fn extract_quads(mut commands: Commands, context: Res<BevyContext<'static>>)
                 quad_type: UIQuadType::Quad,
                 type_index: 0,
                 border_radius: *border_radius,
+                image: None,
             },
         });
     }
diff --git a/bevy_kayak_ui/src/render/unified/shader.wgsl b/bevy_kayak_ui/src/render/unified/shader.wgsl
index 90a5af20ebd89422ce43604ca5bf70c367e7f87c..92b5463abf2909538a8d7ec6f1278d0961aa7f6d 100644
--- a/bevy_kayak_ui/src/render/unified/shader.wgsl
+++ b/bevy_kayak_ui/src/render/unified/shader.wgsl
@@ -13,7 +13,6 @@ struct QuadType {
 [[group(2), binding(0)]]
 var<uniform> quad_type: QuadType;
 
-
 struct VertexOutput {
     [[builtin(position)]] position: vec4<f32>;
     [[location(0)]] color: vec4<f32>;
@@ -44,12 +43,16 @@ fn vertex(
 }
 
 [[group(1), binding(0)]]
-var sprite_texture: texture_2d_array<f32>;
+var font_texture: texture_2d_array<f32>;
 [[group(1), binding(1)]]
-var sprite_sampler: sampler;
+var font_sampler: sampler;
 
-let RADIUS: f32 = 0.1;
+[[group(3), binding(0)]]
+var image_texture: texture_2d<f32>;
+[[group(3), binding(1)]]
+var image_sampler: sampler;
 
+let RADIUS: f32 = 0.1;
 
 fn sd_box_rounded(
     frag_coord: vec2<f32>,
@@ -71,9 +74,6 @@ fn sd_box_rounded(
 
 [[stage(fragment)]]
 fn fragment(in: VertexOutput) -> [[location(0)]] vec4<f32> {
-    var pxRange = 2.5;
-    var tex_dimensions = textureDimensions(sprite_texture);
-    var msdfUnit = vec2<f32>(pxRange, pxRange) / vec2<f32>(f32(tex_dimensions.x), f32(tex_dimensions.y));
     if (quad_type.t == 0) {
         var dist = sd_box_rounded(
             in.position.xy,
@@ -89,7 +89,10 @@ fn fragment(in: VertexOutput) -> [[location(0)]] vec4<f32> {
         return vec4<f32>(in.color.rgb, dist);
     }
     if (quad_type.t == 1) {
-        var x = textureSample(sprite_texture, sprite_sampler, vec2<f32>(in.uv.x, 1.0 - in.uv.y), i32(in.uv.z)); 
+        var px_range = 2.5;
+        var tex_dimensions = textureDimensions(font_texture);
+        var msdf_unit = vec2<f32>(px_range, px_range) / vec2<f32>(f32(tex_dimensions.x), f32(tex_dimensions.y));
+        var x = textureSample(font_texture, font_sampler, vec2<f32>(in.uv.x, 1.0 - in.uv.y), i32(in.uv.z)); 
         var v = max(min(x.r, x.g), min(max(x.r, x.g), x.b));
         var c = v; //remap(v);
 
@@ -103,10 +106,14 @@ fn fragment(in: VertexOutput) -> [[location(0)]] vec4<f32> {
         // var w = fwidth(c);
         // var a = smoothStep(0.5 - w, 0.5 + w, c);
 
-        var sigDist = (c - 0.5) * dot(msdfUnit, 0.5 / fwidth(in.uv.xy));
-        var a = clamp(sigDist + 0.5, 0.0, 1.0);
+        var sig_dist = (c - 0.5) * dot(msdf_unit, 0.5 / fwidth(in.uv.xy));
+        var a = clamp(sig_dist + 0.5, 0.0, 1.0);
 
         return vec4<f32>(in.color.rgb, a);
     }
+    if (quad_type.t == 2) {
+        var color = textureSample(image_texture, image_sampler, vec2<f32>(in.uv.x, in.uv.y));
+        return vec4<f32>(color.rgb * in.color.rgb, color.a * in.color.a);
+    }
     return in.color;
 }
\ No newline at end of file
diff --git a/examples/counter.rs b/examples/counter.rs
index cc68c29f9d3e098742bb8abda0c0612ba704cb7e..2d946636fd5a0c8df3f5ea2a756004935f95e975 100644
--- a/examples/counter.rs
+++ b/examples/counter.rs
@@ -7,6 +7,7 @@ use bevy::{
 use bevy_kayak_ui::{BevyContext, BevyKayakUIPlugin, UICameraBundle};
 use kayak_components::{Button, Text, Window};
 use kayak_core::{
+    context::KayakContext,
     styles::{Style, StyleProp, Units},
     EventType, Index, OnEvent,
 };
@@ -14,7 +15,7 @@ use kayak_ui::components::App;
 use kayak_ui::core::{rsx, widget};
 
 #[widget]
-fn Counter() {
+fn Counter(context: &mut KayakContext) {
     let count = {
         let x = context.create_state(0i32).unwrap();
         *x
diff --git a/examples/image.rs b/examples/image.rs
new file mode 100644
index 0000000000000000000000000000000000000000..82b571488c00812cdf44bd7713a2d89daec7beed
--- /dev/null
+++ b/examples/image.rs
@@ -0,0 +1,55 @@
+use bevy::{
+    math::Vec2,
+    prelude::{App as BevyApp, AssetServer, Commands, Handle, Res, ResMut},
+    window::{WindowDescriptor, Windows},
+    PipelinedDefaultPlugins,
+};
+use bevy_kayak_ui::{BevyContext, BevyKayakUIPlugin, ImageManager, UICameraBundle};
+use kayak_components::Image;
+use kayak_core::Index;
+use kayak_ui::components::App;
+use kayak_ui::core::rsx;
+
+fn startup(
+    mut commands: Commands,
+    windows: Res<Windows>,
+    asset_server: Res<AssetServer>,
+    mut image_manager: ResMut<ImageManager>,
+) {
+    commands.spawn_bundle(UICameraBundle::new());
+
+    let window_size = if let Some(window) = windows.get_primary() {
+        Vec2::new(window.width(), window.height())
+    } else {
+        panic!("Couldn't find primary window!");
+    };
+
+    let handle: Handle<bevy::render2::texture::Image> = asset_server.load("panel.png");
+    let ui_image_handle = image_manager.get(&handle);
+
+    let context = BevyContext::new(window_size.x, window_size.y, |styles, context| {
+        // Hack to trick the proc macro for right now..
+        let parent_id: Option<Index> = None;
+        rsx! {
+            <App styles={Some(styles.clone())}>
+                <Image handle={ui_image_handle} />
+            </App>
+        }
+    });
+
+    commands.insert_resource(context);
+}
+
+fn main() {
+    BevyApp::new()
+        .insert_resource(WindowDescriptor {
+            width: 1270.0,
+            height: 720.0,
+            title: String::from("UI Example"),
+            ..Default::default()
+        })
+        .add_plugins(PipelinedDefaultPlugins)
+        .add_plugin(BevyKayakUIPlugin)
+        .add_startup_system(startup)
+        .run();
+}
diff --git a/kayak_components/src/image.rs b/kayak_components/src/image.rs
index f67f2934983a6a89ff3914bb366331ec90a96587..39cd65c8b5d4c25aa3c4edf30987d5b1ae440dd8 100644
--- a/kayak_components/src/image.rs
+++ b/kayak_components/src/image.rs
@@ -1,9 +1,14 @@
-use kayak_core::{component, rsx, Render, Update};
+use kayak_core::{
+    render_command::RenderCommand,
+    rsx,
+    styles::{Style, StyleProp},
+    widget, Children,
+};
 
-#[component]
-pub fn Image<Children: Render + Update + Clone>(handle: u16, children: Children) {
+#[widget]
+pub fn Image(handle: u16, children: Children) {
     *styles = Some(Style {
-        render_command: StyleProp::Value(RenderCommand::Image { handle }),
+        render_command: StyleProp::Value(RenderCommand::Image { handle: *handle }),
         ..styles.clone().unwrap_or_default()
     });
 
diff --git a/kayak_components/src/lib.rs b/kayak_components/src/lib.rs
index a9b99766d6e61c29c1503cd17f00736729100b40..47472bd1936959d912f6b79d91d9e5cf40feb7c6 100644
--- a/kayak_components/src/lib.rs
+++ b/kayak_components/src/lib.rs
@@ -2,6 +2,7 @@ mod app;
 mod background;
 mod button;
 mod clip;
+mod image;
 mod text;
 mod window;
 
@@ -9,5 +10,6 @@ pub use app::*;
 pub use background::*;
 pub use button::*;
 pub use clip::*;
+pub use image::*;
 pub use text::*;
 pub use window::*;
diff --git a/kayak_core/src/context.rs b/kayak_core/src/context.rs
index bcd89da329aec8ecaf96a8c2d6f6af4f066e5e6d..7e39265f163bc81aef7ab29994743e58ea513ced 100644
--- a/kayak_core/src/context.rs
+++ b/kayak_core/src/context.rs
@@ -1,21 +1,17 @@
-use std::collections::HashMap;
-
-use as_any::AsAny;
 use resources::Ref;
+use std::collections::HashMap;
 
 use crate::{node::NodeIndex, widget_manager::WidgetManager, Event, EventType, Index, InputEvent};
 
-pub trait GlobalState: Send + Sync {}
-
-pub struct KayakContext<'a> {
+pub struct KayakContext {
     component_states: HashMap<crate::Index, resources::Resources>,
     current_id: crate::Index,
     pub widget_manager: WidgetManager,
     last_mouse_position: (f32, f32),
-    global_state: Option<&'a mut dyn GlobalState>,
+    pub global_state: resources::Resources,
 }
 
-impl<'a> std::fmt::Debug for KayakContext<'a> {
+impl std::fmt::Debug for KayakContext {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         f.debug_struct("KayakContext")
             .field("current_id", &self.current_id)
@@ -23,14 +19,14 @@ impl<'a> std::fmt::Debug for KayakContext<'a> {
     }
 }
 
-impl<'a> KayakContext<'a> {
+impl KayakContext {
     pub fn new() -> Self {
         Self {
             component_states: HashMap::new(),
             current_id: crate::Index::default(),
             widget_manager: WidgetManager::new(),
             last_mouse_position: (0.0, 0.0),
-            global_state: None,
+            global_state: resources::Resources::default(),
         }
     }
 
@@ -83,9 +79,21 @@ impl<'a> KayakContext<'a> {
         }
     }
 
-    pub fn render(&mut self, global_state: &'a mut dyn GlobalState) {
-        self.global_state = Some(global_state);
+    pub fn set_global_state<T: resources::Resource>(&mut self, state: T) {
+        self.global_state.insert(state);
+    }
 
+    pub fn get_global_state<T: resources::Resource>(
+        &mut self,
+    ) -> Result<resources::RefMut<T>, resources::CantGetResource> {
+        self.global_state.get_mut::<T>()
+    }
+
+    pub fn take_global_state<T: resources::Resource>(&mut self) -> Option<T> {
+        self.global_state.remove::<T>()
+    }
+
+    pub fn render(&mut self) {
         let dirty_nodes = self.widget_manager.dirty_nodes.clone();
         for node_index in dirty_nodes {
             if self
@@ -104,8 +112,6 @@ impl<'a> KayakContext<'a> {
 
         self.widget_manager.render();
         self.widget_manager.calculate_layout();
-
-        self.global_state = None;
     }
 
     pub fn process_events(&mut self, input_events: Vec<InputEvent>) {
diff --git a/kayak_core/src/lib.rs b/kayak_core/src/lib.rs
index 268b555f0747cfb9eebce923067bba9fa3f08478..fe2cc7fdd442504763f3577a545d658567a24908 100644
--- a/kayak_core/src/lib.rs
+++ b/kayak_core/src/lib.rs
@@ -22,15 +22,10 @@ pub use input_event::*;
 pub use kayak_render_macros::{render, rsx, widget};
 pub use widget::Widget;
 
-pub type Children = Option<
-    Arc<
-        dyn for<'global_state> Fn(
-                Option<crate::Index>,
-                &mut crate::context::KayakContext,
-            ) + Send
-            + Sync,
-    >,
->;
+pub use resources::Resources;
+
+pub type Children =
+    Option<Arc<dyn Fn(Option<crate::Index>, &mut crate::context::KayakContext) + Send + Sync>>;
 
 #[derive(Clone)]
 pub struct OnEvent(
diff --git a/kayak_core/src/render_primitive.rs b/kayak_core/src/render_primitive.rs
index 433c21687c34026412005f73906c85a24e62d140..457c75e4cfb5791651a653b2847281255fe519fc 100644
--- a/kayak_core/src/render_primitive.rs
+++ b/kayak_core/src/render_primitive.rs
@@ -24,6 +24,7 @@ pub enum RenderPrimitive {
         font: u16,
     },
     Image {
+        layout: Rect,
         handle: u16,
     },
 }
@@ -34,6 +35,7 @@ impl RenderPrimitive {
             RenderPrimitive::Clip { layout, .. } => *layout = new_layout,
             RenderPrimitive::Quad { layout, .. } => *layout = new_layout,
             RenderPrimitive::Text { layout, .. } => *layout = new_layout,
+            RenderPrimitive::Image { layout, .. } => *layout = new_layout,
             _ => (),
         }
     }
@@ -71,7 +73,10 @@ impl From<&Style> for RenderPrimitive {
                 content,
                 font,
             },
-            RenderCommand::Image { handle } => Self::Image { handle },
+            RenderCommand::Image { handle } => Self::Image {
+                layout: Rect::default(),
+                handle,
+            },
         }
     }
 }
diff --git a/kayak_render_macros/src/widget.rs b/kayak_render_macros/src/widget.rs
index 96d0c703d7621ead8ad5486a71f92e408038bbb9..1368e1875e472706f59f014cb63c7f042cde7c8e 100644
--- a/kayak_render_macros/src/widget.rs
+++ b/kayak_render_macros/src/widget.rs
@@ -10,7 +10,6 @@ use crate::{tags::OpenTag, widget_attributes::WidgetAttributes};
 
 #[derive(Debug, Clone)]
 pub struct Widget {
-    name: syn::Path,
     pub attributes: WidgetAttributes,
     pub children: Children,
     declaration: TokenStream,
@@ -82,7 +81,6 @@ impl Widget {
         };
 
         Ok(Widget {
-            name,
             attributes: open_tag.attributes,
             children,
             declaration,