diff --git a/assets/texture_atlas.png b/assets/texture_atlas.png
new file mode 100644
index 0000000000000000000000000000000000000000..939cb2835adae278f6ac31d8ea82e21ba3f9139a
Binary files /dev/null and b/assets/texture_atlas.png differ
diff --git a/bevy_kayak_ui/src/render/mod.rs b/bevy_kayak_ui/src/render/mod.rs
index 4a60b6e0d9b9f97f4294a511c9f5c3f69c6b82e1..aa55c2b1b13766b39795a6998215efa035ffc96d 100644
--- a/bevy_kayak_ui/src/render/mod.rs
+++ b/bevy_kayak_ui/src/render/mod.rs
@@ -16,6 +16,7 @@ use kayak_font::KayakFont;
 pub mod font;
 pub mod image;
 mod nine_patch;
+mod texture_atlas;
 mod quad;
 
 pub struct BevyKayakUIExtractPlugin;
@@ -79,6 +80,11 @@ pub fn extract(
                     nine_patch::extract_nine_patch(&render_primitive, &image_manager, &images, dpi);
                 extracted_quads.extend(nine_patch_quads);
             }
+            RenderPrimitive::TextureAtlas { .. } => {
+                let texture_atlas_quads =
+                    texture_atlas::extract_texture_atlas(&render_primitive, &image_manager, &images, dpi);
+                extracted_quads.extend(texture_atlas_quads);
+            }
             RenderPrimitive::Clip { layout } => {
                 extracted_quads.push(ExtractQuadBundle {
                     extracted_quad: ExtractedQuad {
diff --git a/bevy_kayak_ui/src/render/texture_atlas/extract.rs b/bevy_kayak_ui/src/render/texture_atlas/extract.rs
new file mode 100644
index 0000000000000000000000000000000000000000..efe552e9d5dc4848df4f4de3b0e5ef4ba71c792d
--- /dev/null
+++ b/bevy_kayak_ui/src/render/texture_atlas/extract.rs
@@ -0,0 +1,80 @@
+use crate::ImageManager;
+use bevy::{
+    math::Vec2,
+    prelude::{Assets, Res},
+    render::{color::Color, texture::Image},
+    sprite::Rect,
+};
+use bevy_kayak_renderer::{
+    render::unified::pipeline::{ExtractQuadBundle, ExtractedQuad, UIQuadType},
+    Corner,
+};
+use kayak_core::render_primitive::RenderPrimitive;
+
+pub fn extract_texture_atlas(
+    render_primitive: &RenderPrimitive,
+    image_manager: &Res<ImageManager>,
+    images: &Res<Assets<Image>>,
+    dpi: f32,
+) -> Vec<ExtractQuadBundle> {
+    let mut extracted_quads = Vec::new();
+
+    let (size, position, layout, handle) = match render_primitive {
+        RenderPrimitive::TextureAtlas {
+        size,
+        position,
+            layout,
+            handle,
+        } => (size, position, layout, handle),
+        _ => panic!(""),
+    };
+
+    let image_handle = image_manager
+        .get_handle(handle)
+        .and_then(|a| Some(a.clone_weak()));
+
+    let image = images.get(image_handle.as_ref().unwrap());
+
+    if image.is_none() {
+        return vec![];
+    }
+
+    let image_size = image
+        .and_then(|i| {
+            Some(Vec2::new(
+                i.texture_descriptor.size.width as f32,
+                i.texture_descriptor.size.height as f32,
+            ))
+        })
+        .unwrap()
+        * dpi;
+
+    let quad = ExtractQuadBundle {
+        extracted_quad: ExtractedQuad {
+            rect: Rect {
+                min: Vec2::new(layout.posx, layout.posy),
+                max: Vec2::new(layout.posx + layout.width, layout.posy + layout.height),
+            },
+            uv_min: Some(Vec2::new(
+                position.0 / image_size.x,
+                1.0 - ((position.1 + size.1) / image_size.y)
+            )),
+            uv_max: Some(Vec2::new(
+                (position.0 + size.0) / image_size.x,
+                1.0 - (position.1 / image_size.y),
+            )),
+        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: Corner::default(),
+        image: image_handle,
+        },
+    };
+    extracted_quads.push(quad);
+
+    extracted_quads
+}
diff --git a/bevy_kayak_ui/src/render/texture_atlas/mod.rs b/bevy_kayak_ui/src/render/texture_atlas/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..bd5825ad3486b72cd8bdbdd548773246edcc49cd
--- /dev/null
+++ b/bevy_kayak_ui/src/render/texture_atlas/mod.rs
@@ -0,0 +1,2 @@
+mod extract;
+pub use extract::extract_texture_atlas;
diff --git a/kayak_core/src/render_command.rs b/kayak_core/src/render_command.rs
index 6a7b40880451809cb1fe4de1aa6fc7da0a97774d..13f9cbaee03ea17dfc83f3686e6a774014889f9a 100644
--- a/kayak_core/src/render_command.rs
+++ b/kayak_core/src/render_command.rs
@@ -13,6 +13,11 @@ pub enum RenderCommand {
     Image {
         handle: u16,
     },
+    TextureAtlas {
+        position: (f32, f32),
+        size: (f32, f32),
+        handle: u16,
+    },
     NinePatch {
         border: Edge<f32>,
         handle: u16,
diff --git a/kayak_core/src/render_primitive.rs b/kayak_core/src/render_primitive.rs
index 127c3060bde68d3b211a13a41b65ac9ae052d283..11a589ce77a2018e485c9c46bbef2004398a5e83 100644
--- a/kayak_core/src/render_primitive.rs
+++ b/kayak_core/src/render_primitive.rs
@@ -32,6 +32,12 @@ pub enum RenderPrimitive {
         layout: Rect,
         handle: u16,
     },
+    TextureAtlas {
+        size: (f32, f32),
+        position: (f32, f32),
+        layout: Rect,
+        handle: u16,
+    },
     NinePatch {
         border: Edge<f32>,
         layout: Rect,
@@ -47,6 +53,7 @@ impl RenderPrimitive {
             RenderPrimitive::Text { layout, .. } => *layout = new_layout,
             RenderPrimitive::Image { layout, .. } => *layout = new_layout,
             RenderPrimitive::NinePatch { layout, .. } => *layout = new_layout,
+            RenderPrimitive::TextureAtlas { layout, .. } => *layout = new_layout,
             _ => (),
         }
     }
@@ -98,6 +105,12 @@ impl From<&Style> for RenderPrimitive {
                 layout: Rect::default(),
                 handle,
             },
+            RenderCommand::TextureAtlas { handle, size, position,  } => Self::TextureAtlas {
+                handle,
+                layout: Rect::default(),
+                size,
+                position,
+            },
             RenderCommand::NinePatch { handle, border } => Self::NinePatch {
                 border,
                 layout: Rect::default(),
diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs
index 4073a78ccac80c0f1a9c9e4f7ff221395fb12bbd..81ae6a101701076f01ed470510f09c3bc34dbc7a 100644
--- a/src/widgets/mod.rs
+++ b/src/widgets/mod.rs
@@ -8,6 +8,7 @@ mod if_element;
 mod image;
 mod inspector;
 mod nine_patch;
+mod texture_atlas;
 mod scroll;
 mod text;
 mod text_box;
@@ -24,6 +25,7 @@ pub use if_element::*;
 pub use image::*;
 pub use inspector::*;
 pub use nine_patch::*;
+pub use texture_atlas::*;
 pub use scroll::*;
 pub use text::*;
 pub use text_box::*;
diff --git a/src/widgets/texture_atlas.rs b/src/widgets/texture_atlas.rs
new file mode 100644
index 0000000000000000000000000000000000000000..9ea73751671f17c2b5f9e145ea07789d2f97d117
--- /dev/null
+++ b/src/widgets/texture_atlas.rs
@@ -0,0 +1,75 @@
+use kayak_core::OnLayout;
+
+use crate::core::{
+    render_command::RenderCommand,
+    rsx,
+    styles::{Style, StyleProp},
+    widget, Children, OnEvent, WidgetProps,
+};
+
+/// Props used by the [`NinePatch`] widget
+#[derive(WidgetProps, Default, Debug, PartialEq, Clone)]
+pub struct TextureAtlasProps {
+    /// The handle to image
+    pub handle: u16,
+    /// The position of the tile (in pixels)
+    pub position: (f32, f32),
+    /// The size of the tile (in pixels)
+    pub tile_size: (f32, f32),
+    #[prop_field(Styles)]
+    pub styles: Option<Style>,
+    #[prop_field(Children)]
+    pub children: Option<Children>,
+    #[prop_field(OnEvent)]
+    pub on_event: Option<OnEvent>,
+    #[prop_field(OnLayout)]
+    pub on_layout: Option<OnLayout>,
+    #[prop_field(Focusable)]
+    pub focusable: Option<bool>,
+}
+
+#[widget]
+/// A widget that renders a nine-patch image background
+///
+/// A nine-patch is a special type of image that's broken into nine parts:
+///
+/// * Edges - Top, Bottom, Left, Right
+/// * Corners - Top-Left, Top-Right, Bottom-Left, Bottom-Right
+/// * Center
+///
+/// Using these parts of an image, we can construct a scalable background and border
+/// all from a single image. This is done by:
+///
+/// * Stretching the edges (vertically for left/right and horizontally for top/bottom)
+/// * Preserving the corners
+/// * Scaling the center to fill the remaining space
+///
+///
+/// # Props
+///
+/// __Type:__ [`NinePatchProps`]
+///
+/// | Common Prop | Accepted |
+/// | :---------: | :------: |
+/// | `children`  | ✅        |
+/// | `styles`    | ✅        |
+/// | `on_event`  | ✅        |
+/// | `on_layout` | ✅        |
+/// | `focusable` | ✅        |
+///
+pub fn TextureAtlas(props: TextureAtlasProps) {
+    props.styles = Some(Style {
+        render_command: StyleProp::Value(RenderCommand::TextureAtlas {
+            position: props.position,
+            size: props.tile_size,
+            handle: props.handle,
+        }),
+        ..props.styles.clone().unwrap_or_default()
+    });
+
+    rsx! {
+        <>
+            {children}
+        </>
+    }
+}