A comprehensive Qt 6 / QML component library for desktop applications — fully themed, AOT-compiled, and keyboard-friendly.
Mahina is a production-quality component library built on top of Qt 6 and QML. Every component is written to pass the qmlsc AOT strict-mode compiler, uses the shared Theme singleton for consistent design tokens (colours, spacing, radii, typography), and is designed exclusively for keyboard-and-mouse desktop interactions.
Components are registered as a proper QML module (import Mahina) so they drop into any Qt 6 project with a single find_package call. No JavaScript runtime, no web view, no third-party dependencies — just native Qt rendering at full GPU speed.
Mahina started as the UI layer for Qub, a desktop SQL editor built with Qt 6. The goal was a consistent, well-functioning interface — something that felt cohesive rather than a collection of one-off widgets. Qt's built-in QtQuick.Controls covers the basics, but a real application quickly exposes the gaps: no data grid, no command palette, no chart primitives, no diff viewer, no sidebar sections.
After building enough components for Qub, the potential became clear. Other projects were already in mind that would need the same foundations, so the scope grew deliberately — from a handful of purpose-built widgets into a library that could serve as the UI layer for any desktop application.
The result is shared here because others building with Qt 6 are likely running into the same gaps. Mahina won't be the right fit for every project, but if you are looking for a single, coherent component set — one that covers everything from a humble Badge to a full SpreadsheetGrid, all sharing the same theme tokens, interaction conventions and AOT-safe QML patterns — it might save you from reimplementing the same primitives from scratch.
The library was built iteratively with AI assistance. The AI-made column in the component table confirms which components were AI-generated. The Human-reviewed column tracks which ones have been manually verified in a running application.
No local setup required — CMake fetches and builds Mahina automatically at configure time:
include(FetchContent)
FetchContent_Declare(Mahina
GIT_REPOSITORY https://github.com/ajunior/mahina.git
GIT_TAG v0.45.0
)
FetchContent_MakeAvailable(Mahina)
target_link_libraries(MyApp PRIVATE
Qt6::Quick Mahina Mahinaplugin Mahinaplugin_init)
Clone the repo alongside your project and add it directly:
add_subdirectory(mahina)
target_link_libraries(MyApp PRIVATE
Qt6::Quick Mahina Mahinaplugin Mahinaplugin_init)
cmake -B build -DCMAKE_PREFIX_PATH=/path/to/Qt/6.x/gcc_64 \
-DCMAKE_INSTALL_PREFIX=/usr/local
cmake --build build -j$(nproc)
cmake --install build
Then in your project's CMakeLists.txt:
find_package(Mahina REQUIRED)
target_link_libraries(MyApp PRIVATE Mahina::Mahina Mahina::Mahinaplugin)
qt_import_qml_plugins(MyApp)
Mahina ships three font families as embedded Qt resources — no system installation required:
Theme.fontFamilyTheme.fontFamilyMonoIcon componentRegister them with QFontDatabase before loading the QML engine:
#include <QFontDatabase>
// Inter and JetBrains Mono (UI and mono fonts)
QFontDatabase::addApplicationFont(":/qt/qml/Mahina/assets/fonts/InterVariable.ttf");
QFontDatabase::addApplicationFont(":/qt/qml/Mahina/assets/fonts/JetBrainsMonoVariable.ttf");
// Phosphor Icons (all six weights)
QFontDatabase::addApplicationFont(":/qt/qml/Mahina/assets/fonts/Phosphor-Regular.ttf");
QFontDatabase::addApplicationFont(":/qt/qml/Mahina/assets/fonts/Phosphor-Thin.ttf");
QFontDatabase::addApplicationFont(":/qt/qml/Mahina/assets/fonts/Phosphor-Light.ttf");
QFontDatabase::addApplicationFont(":/qt/qml/Mahina/assets/fonts/Phosphor-Bold.ttf");
QFontDatabase::addApplicationFont(":/qt/qml/Mahina/assets/fonts/Phosphor-Fill.ttf");
QFontDatabase::addApplicationFont(":/qt/qml/Mahina/assets/fonts/Phosphor-Duotone.ttf");
Register the import path at runtime, then use any component with a single import:
// main.cpp
engine.addImportPath(QStringLiteral("qrc:/qt/qml"));
// any .qml file
import Mahina
Button {
text: "Hello, Mahina!"
variant: Button.Variant.Filled
onClicked: console.log("clicked")
}
MahinaExtras is an optional companion library that ships alongside Mahina and adds native C++ capabilities that are not achievable in pure QML. Mahina itself stays dependency-free and AOT-safe; extras are opt-in.
It is not built by default. Enable it at configure time:
cmake -B build -DMAHINA_EXTRAS=ON
Add the extras targets next to the main Mahina targets in your CMakeLists.txt:
target_link_libraries(MyApp PRIVATE
Qt6::Quick
Mahina Mahinaplugin Mahinaplugin_init
MahinaExtras MahinaExtrasplugin MahinaExtrasplugin_init
)
Attaches to any CodeEditor via its textDocument property and applies token-level syntax colouring in real time. The colour scheme follows GitHub's dark/light palette and switches automatically when darkMode changes.
import Mahina
import MahinaExtras
CodeEditor {
id: editor
language: "sql"
SyntaxHighlighter {
document: editor.textDocument
language: editor.language
darkMode: Theme.dark
}
}
Supported values for language:
| Value(s) | Language |
|---|---|
"sql" | SQL — keywords, types, strings, numbers, -- and /* */ comments |
"qml" "js" "javascript" "ts" "typescript" | QML / JavaScript / TypeScript — keywords, built-in types, property declarations, template literals |
"python" "py" | Python — keywords, decorators, f-strings, # comments |
"json" | JSON — keys (purple), string values, numbers, true / false / null |
"bash" "sh" "shell" | Bash / Shell — keywords, $VAR expansion, strings, # comments |
"cpp" "c++" "c" "h" "hpp" | C / C++ — keywords, STL types, preprocessor directives, // and /* */ comments |
"java" | Java — keywords, standard library types, annotations (@Override), text blocks, // and /* */ comments |
"rust" "rs" | Rust — keywords, primitive and std types, macros (println!), lifetimes, raw strings, // and /* */ comments |
"go" "golang" | Go — keywords, built-in types and functions, raw string literals (backtick), // and /* */ comments |
"html" "htm" | HTML — tags, attribute names and values, DOCTYPE, <!-- --> comments |
"css" "scss" "less" | CSS — at-rules, property names, values, hex colours, pseudo-classes, /* */ comments |
"yaml" "yml" | YAML — keys, quoted strings, anchors & aliases, scalar keywords, # comments |
"xml" "svg" "xaml" | XML — tags, attribute names and values, processing instructions, CDATA, <!-- --> comments |
An empty or unrecognised language value is accepted — the highlighter applies no rules and the editor renders plain monochrome text.
Mahina bundles Phosphor Icons as embedded font assets — 900+ glyphs across six weights (Thin, Light, Regular, Bold, Fill, Duotone) with no external dependencies and zero network requests.
All glyph names are exposed through the Icons singleton as typed string constants. This avoids magic strings and gives you editor autocompletion:
import Mahina
Icon {
name: Icons.magnifyingGlass
size: 24
color: Theme.primary
}
The Icon component renders any glyph at any pixel size and color. The same Icons.* namespace is used everywhere an icon name is accepted — Button, NavigationRail, ContextMenu, and the rest all take an iconName property that resolves through Icons.
To browse available names, see the Icons component in the reference section below, or search for a specific icon using the component search above.
Every component in Mahina reads its colours from the Theme singleton. There are no hardcoded values inside components — swapping the palette in one place updates the entire UI instantly.
Mahina ships two built-in palettes: light and dark. The active mode is controlled by a single flag:
Theme.dark = true // switch to dark
Theme.dark = false // switch to light
Theme.dark = !Theme.dark // toggle
All 262 components react to this change automatically with no additional wiring.
Theme.load(obj) replaces any subset of tokens at runtime. You only provide the keys you want to change — everything else keeps the Mahina default. Mode-independent tokens (primary, success, warning, etc.) sit at the top level. Surface, border and text tokens that vary between light and dark are nested under light and dark keys:
Theme.load({
primary: "#7c3aed",
primaryHover: "#6d28d9",
primaryActive:"#5b21b6",
primarySubtle:"#ede9fe",
light: {
background: "#f8fafc",
panel: "#f1f5f9",
surface: "#ffffff",
border: "#e2e8f0",
textPrimary: "#0f172a",
textSecondary:"#475569"
},
dark: {
background: "#0f172a",
panel: "#1e293b",
surface: "#1e293b",
border: "#334155",
textPrimary: "#f8fafc",
textSecondary:"#94a3b8"
}
})
Dark mode still works after a load() call — toggling Theme.dark switches between your custom light and dark values, not Mahina's originals.
Any .json file with the same structure can be loaded at startup:
// main.qml
Component.onCompleted: {
const xhr = new XMLHttpRequest()
xhr.open("GET", Qt.resolvedUrl("theme.json"))
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE)
Theme.load(JSON.parse(xhr.responseText))
}
xhr.send()
}
Theme.reset() restores every token to the Mahina built-in values. Useful when letting users switch between multiple saved schemas at runtime:
Button { text: "Reset theme"; onClicked: Theme.reset() }
| Token | Type | In schema | Default |
|---|---|---|---|
primary | color | top-level | #5B8DF6 |
primaryHover | color | top-level | #4878E8 |
primaryActive | color | top-level | #3A66D0 |
primarySubtle | color | top-level | #EEF3FD |
success | color | top-level | #59A14F |
warning | color | top-level | #F28E2B |
error | color | top-level | #E15759 |
info | color | top-level | #2196E8 |
background | color | light / dark | #EDEEF6 / #151922 |
panel | color | light / dark | #F4F4FA / #171D27 |
surface | color | light / dark | #FFFFFF / #1E2430 |
surfaceVariant | color | light / dark | #F8F8FD / #222A36 |
border | color | light / dark | #E2E5F0 / #313B4C |
borderStrong | color | light / dark | #C8CEDF / #475569 |
textPrimary | color | light / dark | #1E2030 / #F4F7FB |
textSecondary | color | light / dark | #697180 / #AAB4C4 |
textDisabled | color | light / dark | #9AA3AF / #7F8A9A |
overlay | color | light / dark | #1E203066 / #00000099 |
shadowColor | color | light / dark | #1E2030 / #000000 |
Every value that appears in a component — colour, size, spacing, radius, duration — comes from a named token on the Theme singleton. Using tokens instead of raw values means a single Theme.load() call repaints the entire UI.
| Token | Default | Usage |
|---|---|---|
Theme.primary | #5B8DF6 | Buttons, links, focus rings, active states |
Theme.primaryHover | #4878E8 | Hover state of filled primary elements |
Theme.primaryActive | #3A66D0 | Pressed / active state |
Theme.primarySubtle | #EEF3FD | Selected row backgrounds, ghost hover fills |
Theme.textOnPrimary | #FFFFFF | Text / icons on filled primary backgrounds |
| Token | Default | Subtle variant |
|---|---|---|
Theme.success | #59A14F | #EEF8ED — Theme.successSubtle |
Theme.warning | #F28E2B | #FEF4E7 — Theme.warningSubtle |
Theme.error | #E15759 | #FEF0F0 — Theme.errorSubtle |
Theme.info | #2196E8 | #EFF7FE — Theme.infoSubtle |
These tokens have separate light and dark values and switch automatically with Theme.dark.
| Token | Light | Dark | Usage |
|---|---|---|---|
Theme.background | #EDEEF6 | #151922 | Page / window background |
Theme.panel | #F4F4FA | #171D27 | Sidebars, toolbar strips |
Theme.surface | #FFFFFF | #1E2430 | Cards, dialogs, popovers |
Theme.surfaceVariant | #F8F8FD | #222A36 | Alternate rows, input backgrounds |
Theme.overlay | #1E203066 | #00000099 | Modal / drawer scrim |
Theme.border | #E2E5F0 | #313B4C | Dividers, input outlines |
Theme.borderStrong | #C8CEDF | #475569 | Emphasized borders, focus rings |
| Token | Light | Dark | Usage |
|---|---|---|---|
Theme.textPrimary | #1E2030 | #F4F7FB | Headlines, labels, body copy |
Theme.textSecondary | #697180 | #AAB4C4 | Captions, hints, metadata |
Theme.textDisabled | #9AA3AF | #7F8A9A | Disabled controls, placeholder text |
Theme.shadowColor | #1E2030 | #000000 | DropShadow filter color |
4 px base grid. Use these for margins, paddings and gaps — never raw pixel values.
| Token | Value | Scale |
|---|---|---|
Theme.sp1 | 4 px | |
Theme.sp2 | 8 px | |
Theme.sp3 | 12 px | |
Theme.sp4 | 16 px | |
Theme.sp5 | 20 px | |
Theme.sp6 | 24 px | |
Theme.sp8 | 32 px | |
Theme.sp10 | 40 px | |
Theme.sp12 | 48 px | |
Theme.sp16 | 64 px |
| Token | Value | Preview | Usage |
|---|---|---|---|
Theme.radiusXs | 2 px | Badges, tags | |
Theme.radiusSm | 4 px | Buttons, inputs, small cards | |
Theme.radiusMd | 8 px | Cards, popovers | |
Theme.radiusLg | 12 px | Dialogs, large panels | |
Theme.radiusXl | 16 px | Sheets, drawers | |
Theme.radiusFull | 9999 px | Pills, avatar circles |
Mahina bundles two fonts as embedded assets — no network requests, no system dependency.
| Token | Default | Usage |
|---|---|---|
Theme.fontFamily |
Inter (variable, v4.0) | All UI text — labels, body, headings |
Theme.fontFamilyMono |
JetBrains Mono (variable, v2.3) | Code blocks, terminal, editors, kbd shortcuts |
Both tokens are writable and included in Theme.load(), so you can swap to any font the host app registers:
// in main.cpp
QFontDatabase::addApplicationFont(":/fonts/Geist-Variable.ttf");
// in QML
Theme.load({ fontFamily: "Geist", fontFamilyMono: "Fira Code" })
| Token | Size | Preview |
|---|---|---|
Theme.textXs | 11 px | The quick brown fox |
Theme.textSm | 13 px | The quick brown fox |
Theme.textBase | 15 px | The quick brown fox |
Theme.textLg | 17 px | The quick brown fox |
Theme.textXl | 20 px | The quick brown fox |
Theme.text2xl | 24 px | The quick brown fox |
Theme.text3xl | 30 px | The quick brown fox |
Theme.text4xl | 36 px | The quick brown fox |
| Token | Value | Preview |
|---|---|---|
Theme.weightLight | 300 | Mahina UI |
Theme.weightRegular | 400 | Mahina UI |
Theme.weightMedium | 500 | Mahina UI |
Theme.weightSemibold | 600 | Mahina UI |
Theme.weightBold | 700 | Mahina UI |
| Token | Value | Usage |
|---|---|---|
Theme.durationFast | 80 ms | Hover colour transitions, small state changes |
Theme.durationNormal | 150 ms | Most enter/leave transitions |
Theme.durationSlow | 250 ms | Larger layout changes, page transitions |
Theme.easing | OutCubic | Default easing curve for all animations |
Three shadow levels for use with Qt's DropShadow layer effect. Each level exposes a blur radius, Y offset, and opacity multiplier paired with Theme.shadowColor.
| Level | Blur | Y offset | Opacity | Usage |
|---|---|---|---|---|
shadowSm* | 6 px | 1 px | 0.06 | Cards, inputs |
shadowMd* | 16 px | 4 px | 0.10 | Dropdowns, popovers |
shadowLg* | 32 px | 8 px | 0.14 | Dialogs, floating panels |
| Component | Category | Description | AI-made | Human-reviewed |
|---|
No components match.
Per-component description, implementation notes, and best-fit scenarios for all 262 components.
The repository ships a self-contained kitchen-sink app that renders live instances of the components against a real Qt runtime. It is the fastest way to see how everything looks and behaves together, and to verify that your build environment is set up correctly.
git clone https://github.com/ajunior/mahina.git
cd mahina
cmake -B build -DCMAKE_PREFIX_PATH=/path/to/Qt/6.x/gcc_64
cmake --build build -j$(nproc)
./build/example/bin/MahinaExample
A 1000 × 680 window with a NavBar, a Sidebar, and 18 pages that group components by theme. Each page is a scrollable Flickable with live, interactive instances — not screenshots.
| Page | What it covers |
|---|---|
| Components | Buttons, Badges, Inputs, Cards, Avatars, Chips, Toggles, Checkboxes, Sliders, Dropdowns |
| Notifications | Alert, Banner, Callout, Toast, Notification, Badge counts |
| Typography | Font scale, weights, colour tokens, Kbd, CodeBlock |
| Icons | Icon grid, all six Phosphor weights side by side |
| Forms | FormField, DatePicker, TimePicker, OTPInput, RatingInput, TagInput, MultiSelect |
| Data | DataGrid, VirtualList, Tree, JsonViewer, DataList, Pagination |
| Extended | CommandPalette, Tour, Kanban, EventCalendar, DashboardGrid, Carousel |
| More | Drawer, Sheet, Dialog, Popover, HoverCard, ContextMenu, Tooltip |
| Advanced | SplitPane, Resizable, Dock, FloatingToolbar, FloatingIsland, Sortable |
| Settings | PreferencesLayout, Toggle groups, KeybindingInput, ShortcutManager |
| Display & Data | MediaPlayer, ImageGallery, VideoPlayer, QRCode, Timeline, ActivityFeed |
| Charts & Layout | AreaChart, GanttChart, TreeMap, Histogram, LiveChart, KPICard, MetricCard |
| Charts & Nav | RadarChart, BubbleChart, ScatterPlot, MenuBar, Ribbon, NavigationRail |
| Inputs & Charts | ColorPicker, FontPicker, CronEditor, NodeEditor, KeyframeEditor, SpreadsheetGrid |
| Social & Feedback | Chat, CommentThread, ReactionBar, PresenceList, FeedbackWidget, Confetti |
| Social & Charts | VideoCallTile, NetworkGraph, SankeyDiagram, WordCloud, CorrelationMatrix |
| Nav & Inputs | CommandMenu, TabDock, AppSwitcher, DragDropList, TransferList, FilterBar |
| Editors & Tools | CodeEditor, DiffEditor, HexViewer, Terminal, RichTextEditor, MarkdownEditor |
Press D at any time to toggle between light and dark. The NavBar also exposes a toggle button. All 262 components respond to the Theme.dark flag with no additional wiring.