diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index 6778e984..eca7270d 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -37,6 +37,7 @@ qt_add_library(quickshell-core STATIC
 	common.cpp
 	iconprovider.cpp
 	scriptmodel.cpp
+	colorquantizer.cpp
 )
 
 qt_add_qml_module(quickshell-core
diff --git a/src/core/colorquantizer.cpp b/src/core/colorquantizer.cpp
new file mode 100644
index 00000000..abe6af8e
--- /dev/null
+++ b/src/core/colorquantizer.cpp
@@ -0,0 +1,230 @@
+#include "colorquantizer.hpp"
+
+#include <qcolor.h>
+#include <qlogging.h>
+#include <qloggingcategory.h>
+#include <qobject.h>
+#include <qqmllist.h>
+#include <qthreadpool.h>
+#include <qtypes.h>
+
+namespace {
+Q_LOGGING_CATEGORY(logColorQuantizer, "quickshell.colorquantizer", QtWarningMsg);
+}
+
+ColorQuantizerOperation::ColorQuantizerOperation(QUrl* source, qreal depth, qreal rescaleSize)
+    : source(source)
+    , maxDepth(depth)
+    , rescaleSize(rescaleSize) {
+	setAutoDelete(false);
+}
+
+void ColorQuantizerOperation::quantizeImage(const QAtomicInteger<bool>& shouldCancel) {
+	if (shouldCancel.loadAcquire() || source->isEmpty()) return;
+
+	colors.clear();
+
+	auto image = QImage(source->toLocalFile());
+	if ((image.width() > rescaleSize || image.height() > rescaleSize) && rescaleSize > 0) {
+		image = image.scaled(
+		    static_cast<int>(rescaleSize),
+		    static_cast<int>(rescaleSize),
+		    Qt::KeepAspectRatio,
+		    Qt::SmoothTransformation
+		);
+	}
+
+	if (image.isNull()) {
+		qCWarning(logColorQuantizer) << "Failed to load image from" << source;
+		return;
+	}
+
+	QList<QColor> pixels;
+	for (int y = 0; y != image.height(); ++y) {
+		for (int x = 0; x != image.width(); ++x) {
+			auto pixel = image.pixel(x, y);
+			if (qAlpha(pixel) == 0) continue;
+
+			pixels.append(QColor::fromRgb(pixel));
+		}
+	}
+
+	auto startTime = QDateTime::currentDateTime();
+
+	colors = quantization(pixels, 0);
+
+	auto endTime = QDateTime::currentDateTime();
+	auto milliseconds = startTime.msecsTo(endTime);
+	qCDebug(logColorQuantizer) << "Color Quantization took: " << milliseconds << "ms";
+}
+
+QList<QColor> ColorQuantizerOperation::quantization(
+    QList<QColor>& rgbValues,
+    qreal depth,
+    const QAtomicInteger<bool>& shouldCancel
+) {
+	if (shouldCancel.loadAcquire()) return QList<QColor>();
+
+	if (depth >= maxDepth || rgbValues.isEmpty()) {
+		if (rgbValues.isEmpty()) return QList<QColor>();
+
+		auto totalR = 0;
+		auto totalG = 0;
+		auto totalB = 0;
+
+		for (const auto& color: rgbValues) {
+			if (shouldCancel.loadAcquire()) return QList<QColor>();
+
+			totalR += color.red();
+			totalG += color.green();
+			totalB += color.blue();
+		}
+
+		auto avgColor = QColor(
+		    qRound(totalR / static_cast<double>(rgbValues.size())),
+		    qRound(totalG / static_cast<double>(rgbValues.size())),
+		    qRound(totalB / static_cast<double>(rgbValues.size()))
+		);
+
+		return QList<QColor>() << avgColor;
+	}
+
+	auto dominantChannel = findBiggestColorRange(rgbValues);
+	std::ranges::sort(rgbValues, [dominantChannel](const auto& a, const auto& b) {
+		if (dominantChannel == 'r') return a.red() < b.red();
+		else if (dominantChannel == 'g') return a.green() < b.green();
+		return a.blue() < b.blue();
+	});
+
+	auto mid = rgbValues.size() / 2;
+
+	auto leftHalf = rgbValues.mid(0, mid);
+	auto rightHalf = rgbValues.mid(mid);
+
+	QList<QColor> result;
+	result.append(quantization(leftHalf, depth + 1));
+	result.append(quantization(rightHalf, depth + 1));
+
+	return result;
+}
+
+char ColorQuantizerOperation::findBiggestColorRange(const QList<QColor>& rgbValues) {
+	if (rgbValues.isEmpty()) return 'r';
+
+	auto rMin = 255;
+	auto gMin = 255;
+	auto bMin = 255;
+	auto rMax = 0;
+	auto gMax = 0;
+	auto bMax = 0;
+
+	for (const auto& color: rgbValues) {
+		rMin = qMin(rMin, color.red());
+		gMin = qMin(gMin, color.green());
+		bMin = qMin(bMin, color.blue());
+
+		rMax = qMax(rMax, color.red());
+		gMax = qMax(gMax, color.green());
+		bMax = qMax(bMax, color.blue());
+	}
+
+	auto rRange = rMax - rMin;
+	auto gRange = gMax - gMin;
+	auto bRange = bMax - bMin;
+
+	auto biggestRange = qMax(rRange, qMax(gRange, bRange));
+	if (biggestRange == rRange) {
+		return 'r';
+	} else if (biggestRange == gRange) {
+		return 'g';
+	} else {
+		return 'b';
+	}
+}
+
+void ColorQuantizerOperation::finishRun() {
+	QMetaObject::invokeMethod(this, &ColorQuantizerOperation::finished, Qt::QueuedConnection);
+}
+
+void ColorQuantizerOperation::finished() {
+	emit this->done(colors);
+	delete this;
+}
+
+void ColorQuantizerOperation::run() {
+	if (!this->shouldCancel) {
+		this->quantizeImage();
+
+		if (this->shouldCancel.loadAcquire()) {
+			qCDebug(logColorQuantizer) << "Color quantization" << this << "cancelled";
+		}
+	}
+
+	this->finishRun();
+}
+
+void ColorQuantizerOperation::tryCancel() { this->shouldCancel.storeRelease(true); }
+
+void ColorQuantizer::componentComplete() {
+	componentCompleted = true;
+	if (!mSource.isEmpty()) quantizeAsync();
+}
+
+void ColorQuantizer::setSource(const QUrl& source) {
+	if (mSource != source) {
+		mSource = source;
+		emit this->sourceChanged();
+
+		if (this->componentCompleted && !mSource.isEmpty()) quantizeAsync();
+	}
+}
+
+void ColorQuantizer::setDepth(qreal depth) {
+	if (mDepth != depth) {
+		mDepth = depth;
+		emit this->depthChanged();
+
+		if (this->componentCompleted) quantizeAsync();
+	}
+}
+
+void ColorQuantizer::setRescaleSize(int rescaleSize) {
+	if (mRescaleSize != rescaleSize) {
+		mRescaleSize = rescaleSize;
+		emit this->rescaleSizeChanged();
+
+		if (this->componentCompleted) quantizeAsync();
+	}
+}
+
+void ColorQuantizer::operationFinished(const QList<QColor>& result) {
+	bColors = result;
+	this->liveOperation = nullptr;
+	emit this->colorsChanged();
+}
+
+void ColorQuantizer::quantizeAsync() {
+	if (this->liveOperation) this->cancelAsync();
+
+	qCDebug(logColorQuantizer) << "Starting color quantization asynchronously";
+	this->liveOperation = new ColorQuantizerOperation(&mSource, mDepth, mRescaleSize);
+
+	QObject::connect(
+	    this->liveOperation,
+	    &ColorQuantizerOperation::done,
+	    this,
+	    &ColorQuantizer::operationFinished
+	);
+
+	QThreadPool::globalInstance()->start(this->liveOperation);
+}
+
+void ColorQuantizer::cancelAsync() {
+	if (!this->liveOperation) return;
+
+	this->liveOperation->tryCancel();
+	QThreadPool::globalInstance()->waitForDone();
+
+	QObject::disconnect(this->liveOperation, nullptr, this, nullptr);
+	this->liveOperation = nullptr;
+}
diff --git a/src/core/colorquantizer.hpp b/src/core/colorquantizer.hpp
new file mode 100644
index 00000000..b8456db4
--- /dev/null
+++ b/src/core/colorquantizer.hpp
@@ -0,0 +1,128 @@
+#pragma once
+
+#include <qlist.h>
+#include <qobject.h>
+#include <qproperty.h>
+#include <qqmlintegration.h>
+#include <qqmlparserstatus.h>
+#include <qrunnable.h>
+#include <qtmetamacros.h>
+#include <qtypes.h>
+
+class ColorQuantizerOperation
+    : public QObject
+    , public QRunnable {
+	Q_OBJECT;
+
+public:
+	explicit ColorQuantizerOperation(QUrl* source, qreal depth, qreal rescaleSize);
+
+	void run() override;
+	void tryCancel();
+
+signals:
+	void done(QList<QColor> colors);
+
+private slots:
+	void finished();
+
+private:
+	static char findBiggestColorRange(const QList<QColor>& rgbValues);
+
+	void quantizeImage(const QAtomicInteger<bool>& shouldCancel = false);
+
+	QList<QColor> quantization(
+	    QList<QColor>& rgbValues,
+	    qreal depth,
+	    const QAtomicInteger<bool>& shouldCancel = false
+	);
+
+	void finishRun();
+
+	QAtomicInteger<bool> shouldCancel = false;
+	QList<QColor> colors;
+	QUrl* source;
+	qreal maxDepth;
+	qreal rescaleSize;
+};
+
+///! Color Quantization Utility
+/// A color quantization utility used for getting prevalent colors in an image, by
+/// averaging out the image's color data recursively.
+///
+/// #### Example
+/// ```qml
+/// ColorQuantizer {
+///   id: colorQuantizer
+///   source: Qt.resolvedUrl("./yourImage.png")
+///   depth: 3 // Will produce 8 colors (2³)
+///   rescaleSize: 64 // Rescale to 64x64 for faster processing
+/// }
+/// ```
+class ColorQuantizer
+    : public QObject
+    , public QQmlParserStatus {
+
+	Q_OBJECT;
+	QML_ELEMENT;
+	Q_INTERFACES(QQmlParserStatus);
+	/// Access the colors resulting from the color quantization performed.
+	/// > [!NOTE] The amount of colors returned from the quantization is determined by
+	/// > the property depth, specifically 2ⁿ where n is the depth.
+	Q_PROPERTY(QList<QColor> colors READ default NOTIFY colorsChanged BINDABLE bindableColors);
+
+	/// Path to the image you'd like to run the color quantization on.
+	Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged);
+
+	/// Max depth for the color quantization. Each level of depth represents another
+	/// binary split of the color space
+	Q_PROPERTY(qreal depth READ depth WRITE setDepth NOTIFY depthChanged);
+
+	/// The size to rescale the image to, when rescaleSize is 0 then no scaling will be done.
+	/// > [!NOTE] Results from color quantization doesn't suffer much when rescaling, it's
+	/// > reccommended to rescale, otherwise the quantization process will take much longer.
+	Q_PROPERTY(qreal rescaleSize READ rescaleSize WRITE setRescaleSize NOTIFY rescaleSizeChanged);
+
+public:
+	explicit ColorQuantizer(QObject* parent = nullptr): QObject(parent) {}
+
+	void componentComplete() override;
+	void classBegin() override {}
+
+	[[nodiscard]] QBindable<QList<QColor>> bindableColors() { return &this->bColors; }
+
+	[[nodiscard]] QUrl source() const { return mSource; }
+	void setSource(const QUrl& source);
+
+	[[nodiscard]] qreal depth() const { return mDepth; }
+	void setDepth(qreal depth);
+
+	[[nodiscard]] qreal rescaleSize() const { return mRescaleSize; }
+	void setRescaleSize(int rescaleSize);
+
+signals:
+	void colorsChanged();
+	void sourceChanged();
+	void depthChanged();
+	void rescaleSizeChanged();
+
+public slots:
+	void operationFinished(const QList<QColor>& result);
+
+private:
+	void quantizeAsync();
+	void cancelAsync();
+
+	bool componentCompleted = false;
+	ColorQuantizerOperation* liveOperation = nullptr;
+	QUrl mSource;
+	qreal mDepth = 0;
+	qreal mRescaleSize = 0;
+
+	Q_OBJECT_BINDABLE_PROPERTY(
+	    ColorQuantizer,
+	    QList<QColor>,
+	    bColors,
+	    &ColorQuantizer::colorsChanged
+	);
+};
diff --git a/src/core/module.md b/src/core/module.md
index c8b17ab9..b9404ea9 100644
--- a/src/core/module.md
+++ b/src/core/module.md
@@ -29,5 +29,6 @@ headers = [
 	"qsmenuanchor.hpp",
 	"clock.hpp",
 	"scriptmodel.hpp",
+	"colorquantizer.hpp",
 ]
 -----