How to Create Custom Qt6 Widgets with the Obsidian Design System

To create custom Qt6 widgets with the Obsidian design system, derive from a Qt base class, use token-based helpers from Theme.h (e.g., colors::ACCENT), and apply styles via StyleSheets.cpp helpers or manual palette updates.

FinceptTerminal implements its complete Obsidian design system through code-generated design tokens that centralize every color, font, and spacing value. By leveraging the token architecture in fincept-qt/src/ui/theme/, developers can build custom widgets that automatically adopt the dark-themed aesthetic and respond dynamically to runtime theme changes.

Understanding the Obsidian Design Token Architecture

The Obsidian design system is built on four foundational components that separate definition from implementation:

  • ThemeTokens.h – Defines the ThemeTokens struct containing raw values for every color, font size, and spacing constant used across the application.
  • Theme.h – Exposes lightweight ColorToken and FontToken wrappers through the colors:: and fonts:: namespaces. These tokens resolve to current runtime values on access.
  • ThemeManager.cpp – Maintains the active theme instance and emits the theme_changed signal whenever the user adjusts density, fonts, or color presets.
  • StyleSheets.cpp – Provides pre-baked QSS generator functions like styles::button() and styles::card_frame() that embed token references.

Because tokens resolve dynamically, a widget painted with colors::BG_BASE automatically updates if the user switches from Obsidian to a future light-mode variant.

Step-by-Step Implementation Guide

Include the Token System Headers

Start by including the runtime token accessors and stylesheet helpers:

#include "ui/theme/Theme.h"
#include "ui/theme/StyleSheets.h"

The colors:: namespace provides constexpr instances such as colors::TEXT_PRIMARY, colors::ACCENT, and colors::BORDER, while fonts:: exposes typography tokens. These objects implicitly convert to QColor, QString, or int as needed.

Derive from Qt Base Classes

Create your widget by inheriting from standard Qt6 classes like QWidget, QFrame, or QPushButton. For interactive elements, QPushButton provides the necessary event handling infrastructure.

Apply Token-Based Styling

Use one of two approaches to apply the Obsidian aesthetic:

Stylesheet approach – Call helpers from StyleSheets.cpp:

setStyleSheet(styles::button());  // Embeds %9 (accent), %10 (secondary text), etc.

Palette approach – Modify the widget palette directly:

QPalette p = palette();
p.setColor(QPalette::Window, colors::BG_BASE);
p.setColor(QPalette::ButtonText, colors::TEXT_PRIMARY);
setPalette(p);

Handle Runtime Theme Changes

Connect to ThemeManager::theme_changed to refresh styling when the global theme updates:

connect(&ThemeManager::instance(), &ThemeManager::theme_changed,
        this, &YourWidget::applyStyle);

The applyStyle() slot should re-apply all token-dependent colors to ensure consistency.

Complete Code Examples

Minimal ObsidianButton Implementation

This example demonstrates a QPushButton derivative that uses the built-in button stylesheet and updates its palette when themes change.

Header (src/ui/widgets/ObsidianButton.h):

#pragma once
#include <QPushButton>
#include "ui/theme/Theme.h"

namespace fincept::ui {

class ObsidianButton : public QPushButton {
    Q_OBJECT
public:
    explicit ObsidianButton(const QString& text, QWidget* parent = nullptr);

private:
    void applyStyle();
};

} // namespace fincept::ui

Implementation (src/ui/widgets/ObsidianButton.cpp):

#include "ObsidianButton.h"
#include "ui/theme/StyleSheets.h"

namespace fincept::ui {

ObsidianButton::ObsidianButton(const QString& text, QWidget* parent)
    : QPushButton(text, parent) {
    setStyleSheet(styles::button());
    applyStyle();

    connect(&ThemeManager::instance(), &ThemeManager::theme_changed,
            this, &ObsidianButton::applyStyle);
}

void ObsidianButton::applyStyle() {
    QPalette p = palette();
    p.setColor(QPalette::ButtonText, colors::TEXT_PRIMARY);
    setPalette(p);
}

} // namespace fincept::ui

The styles::button() function references token placeholders that ThemeManager populates from the active ThemeTokens structure.

Complex InfoCard with Dynamic Updates

This QFrame derivative mimics the built-in Card class from fincept-qt/src/ui/widgets/Card.cpp, demonstrating manual layout construction with token-aware styling.

Header (src/ui/widgets/InfoCard.h):

#pragma once
#include <QFrame>
#include <QVBoxLayout>
#include <QLabel>
#include "ui/theme/Theme.h"

namespace fincept::ui {

class InfoCard : public QFrame {
    Q_OBJECT
public:
    explicit InfoCard(const QString& title, QWidget* parent = nullptr);
    void setInfo(const QString& label, const QString& value);

private:
    QLabel* titleLabel_;
    QLabel* label_;
    QLabel* value_;
    QVBoxLayout* contentLayout_;

    void applyStyle();
};

} // namespace fincept::ui

Implementation (src/ui/widgets/InfoCard.cpp):

#include "InfoCard.h"
#include "ui/theme/StyleSheets.h"

namespace fincept::ui {

InfoCard::InfoCard(const QString& title, QWidget* parent)
    : QFrame(parent) {
    setStyleSheet(styles::card_frame());
    auto* vl = new QVBoxLayout(this);
    vl->setContentsMargins(0,0,0,0);
    vl->setSpacing(0);

    // Title bar construction
    auto* titleBar = new QWidget;
    titleBar->setFixedHeight(28);
    titleBar->setStyleSheet("background: transparent;");
    auto* hl = new QHBoxLayout(titleBar);
    hl->setContentsMargins(8,0,4,0);
    titleLabel_ = new QLabel(title);
    titleLabel_->setStyleSheet(styles::card_title());
    hl->addWidget(titleLabel_);
    hl->addStretch();
    vl->addWidget(titleBar);

    // Separator using border token
    auto* sep = new QFrame;
    sep->setFixedHeight(1);
    sep->setStyleSheet(QString("background: %1; border: none;")
                       .arg(colors::BORDER()));
    vl->addWidget(sep);

    // Content area
    auto* content = new QWidget;
    content->setStyleSheet("background: transparent;");
    contentLayout_ = new QVBoxLayout(content);
    contentLayout_->setContentsMargins(8,4,8,8);
    label_ = new QLabel;
    value_ = new QLabel;
    label_->setStyleSheet(QString("color: %1;").arg(colors::TEXT_SECONDARY));
    value_->setStyleSheet(QString("color: %1; font-weight: 600;").arg(colors::TEXT_PRIMARY));
    contentLayout_->addWidget(label_);
    contentLayout_->addWidget(value_);
    vl->addWidget(content, 1);

    applyStyle();

    connect(&ThemeManager::instance(), &ThemeManager::theme_changed,
            this, &InfoCard::applyStyle);
}

void InfoCard::setInfo(const QString& label, const QString& value) {
    label_->setText(label);
    value_->setText(value);
}

void InfoCard::applyStyle() {
    titleLabel_->setStyleSheet(QString("color: %1; background: %2;")
                               .arg(colors::TEXT_ON_ACCENT)
                               .arg(colors::ACCENT()));
}

} // namespace fincept::ui

The InfoCard class uses colors::TEXT_SECONDARY for metadata and colors::ACCENT for highlights, ensuring consistency with the Obsidian palette defined in ThemeTokens.h.

Key Source Files in the Obsidian Design System

Refer to these implementation files when building custom widgets:

Summary

  • Derive from Qt base classes (QWidget, QFrame, QPushButton) to maintain standard Qt6 event handling.
  • Access design tokens through the colors:: and fonts:: namespaces in Theme.h, which resolve to current values at runtime.
  • Apply styles using either StyleSheets.cpp helpers (for complex QSS) or direct palette manipulation with token values.
  • Subscribe to theme changes via ThemeManager::theme_changed to ensure widgets update when the user modifies the global theme.

Frequently Asked Questions

How do I access color tokens in a custom Qt6 widget?

Include ui/theme/Theme.h and use the colors:: namespace. Tokens like colors::BG_BASE, colors::TEXT_PRIMARY, and colors::ACCENT convert implicitly to QColor or QString when passed to Qt APIs. For example, palette.setColor(QPalette::Window, colors::BG_BASE) sets the background using the current theme's base color.

What is the difference between StyleSheets.cpp and manual palette updates?

StyleSheets.cpp provides pre-built QSS strings via functions like styles::button() and styles::card_frame(), which embed token references for complex styling rules. Manual palette updates using QPalette are preferable for simple color changes or when you need to modify specific widget roles without parsing CSS-like syntax.

How does the Obsidian design system handle theme switching at runtime?

ThemeManager holds the active ThemeTokens instance and rebuilds the global stylesheet when settings change. Custom widgets should connect to the theme_changed signal and re-apply token-dependent colors in their applyStyle() slots. Because tokens resolve dynamically on each access, widgets automatically reflect the new theme without hard-coded color values.

Where are the base design tokens defined in FinceptTerminal?

All base values reside in fincept-qt/src/ui/theme/ThemeTokens.h as a plain C++ struct. This file defines every color (e.g., background, accent, semantic states like positive and negative), font sizes, and spacing constants used throughout the application. The ThemeManager loads these values into the colors:: and fonts:: namespaces at runtime.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →