diff --git a/include/utils/color.hpp b/include/utils/color.hpp
new file mode 100644
index 00000000..1f116b9e
--- /dev/null
+++ b/include/utils/color.hpp
@@ -0,0 +1,97 @@
+#pragma once
+
+#include <iomanip>
+
+#include "common.hpp"
+#include "utils/string.hpp"
+
+LEMONBUDDY_NS
+
+namespace color_util {
+  template <typename ChannelType = uint8_t, typename ValueType = uint32_t>
+  struct color {
+    using type = color<ChannelType, ValueType>;
+    union {
+      struct {
+        ChannelType red;
+        ChannelType green;
+        ChannelType blue;
+        ChannelType alpha;
+      } bits;
+
+      ValueType value = 0U;
+    } colorspace;
+
+    explicit color(ValueType v) {
+      colorspace.value = v;
+    }
+  };
+
+  template <typename T>
+  auto make_24bit(T&& value) {
+    return color<uint8_t, uint32_t>(forward<T>(value));
+  }
+
+  template <typename T>
+  auto make_32bit(T&& value) {
+    return color<uint16_t, uint32_t>(forward<T>(value));
+  }
+
+  template <typename ValueType = uint32_t>
+  uint8_t alpha(const color<ValueType> c) {
+    return ((c.colorspace.value >> 24) << 8) | ((c.colorspace.value >> 24));
+  }
+
+  template <typename T = uint8_t, typename ValueType = uint32_t>
+  T red(const color<ValueType> c) {
+    uint8_t r = c.colorspace.value >> 16;
+    if (std::is_same<T, uint8_t>::value)
+      return r << 8 / 0xFF;
+    if (std::is_same<T, uint16_t>::value)
+      return r << 8 | r << 8 / 0xFF;
+  }
+
+  template <typename T = uint8_t, typename ValueType = uint32_t>
+  T green(const color<ValueType> c) {
+    uint8_t g = c.colorspace.value >> 8;
+    if (std::is_same<T, uint8_t>::value)
+      return g << 8 / 0xFF;
+    if (std::is_same<T, uint16_t>::value)
+      return g << 8 | g << 8 / 0xFF;
+  }
+
+  template <typename T = uint8_t, typename ValueType = uint32_t>
+  T blue(const color<ValueType> c) {
+    uint8_t b = c.colorspace.value;
+    if (std::is_same<T, uint8_t>::value)
+      return b << 8 / 0xFF;
+    if (std::is_same<T, uint16_t>::value)
+      return b << 8 | b << 8 / 0xFF;
+  }
+
+  string hex(const color<uint8_t, uint32_t> value) {
+    // clang-format off
+    return string_util::from_stream(stringstream()
+        << "#"
+        << std::setw(6)
+        << std::setfill('0')
+        << std::hex
+        << std::uppercase
+        << (value.colorspace.value & 0x00FFFFFF));
+    // clang-format on
+  }
+
+  string hex(const color<uint16_t, uint32_t> value) {
+    // clang-format off
+    return string_util::from_stream(stringstream()
+        << "#"
+        << std::setw(8)
+        << std::setfill('0')
+        << std::hex
+        << std::uppercase
+        << value.colorspace.value);
+    // clang-format on
+  }
+}
+
+LEMONBUDDY_NS_END
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index e6317366..f98b5845 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -12,6 +12,7 @@ function(unit_test file)
   add_test(unit_test.${testname} unit_test.${testname})
 endfunction()
 
+unit_test("utils/color")
 unit_test("utils/math")
 unit_test("utils/memory")
 unit_test("utils/string")
diff --git a/tests/unit_tests/utils/color.cpp b/tests/unit_tests/utils/color.cpp
new file mode 100644
index 00000000..c1fea25b
--- /dev/null
+++ b/tests/unit_tests/utils/color.cpp
@@ -0,0 +1,29 @@
+#include "common/test.hpp"
+#include "utils/color.hpp"
+
+int main() {
+  using namespace lemonbuddy;
+
+  "rgb"_test = []{
+    auto color = color_util::make_24bit(0x123456);
+    expect(color_util::alpha(color) == 0);
+    expect(color_util::red<uint8_t>(color) == 0x12);
+    expect(color_util::green<uint8_t>(color) == 0x34);
+    expect(color_util::blue<uint8_t>(color) == 0x56);
+  };
+
+  "rgba"_test = []{
+    auto color = color_util::make_32bit(0xCC123456);
+    expect(color_util::alpha(color) == 0xCC);
+    expect(color_util::red<uint16_t>(color) == 0x1212);
+    expect(color_util::green<uint16_t>(color) == 0x3434);
+    expect(color_util::blue<uint16_t>(color) == 0x5656);
+  };
+
+  "hex"_test = [] {
+    auto colorA = color_util::make_24bit(0x123456);
+    expect(color_util::hex(colorA) == "#123456");
+    auto colorB = color_util::make_32bit(0xCC123456);
+    expect(color_util::hex(colorB) == "#CC123456");
+  };
+}