No description
  • Rust 97.6%
  • CSS 2.4%
Find a file
Karl Wahlström a20a117fad
Some checks failed
CI / check (push) Has been cancelled
Added alot of properties.
2026-07-20 20:28:09 +02:00
.agents/skills/qtml Added alot of properties. 2026-07-20 20:28:09 +02:00
.github/workflows Added licence and stuff. 2026-07-14 17:44:58 +02:00
examples Revamped language so that style is generated by language instead of 2026-07-18 21:19:16 +02:00
qtml_build Changed how some component defaults are handled. 2026-07-20 15:08:38 +02:00
qtml_core Added alot of properties. 2026-07-20 20:28:09 +02:00
qtml_lsp Added defaults functionality. 2026-07-20 13:58:14 +02:00
qtml_macro Changed how some component defaults are handled. 2026-07-20 15:08:38 +02:00
.gitignore First big commit. 2026-07-12 16:36:23 +02:00
AGENTS.md Added conditionals for style properties. 2026-07-20 13:16:55 +02:00
Cargo.lock Revamped language so that style is generated by language instead of 2026-07-18 21:19:16 +02:00
Cargo.toml Revamped language so that style is generated by language instead of 2026-07-18 21:19:16 +02:00
LICENSE Added licence and stuff. 2026-07-14 17:44:58 +02:00
README.md Changed how mobile and desctio work. 2026-07-20 15:16:29 +02:00

QTML

QTML is a QML-inspired template language for compiling server-rendered Rust templates and native CSS.

The primary entry point is the render_qtml! macro from qtml_macro.

use qtml_macro::{render_qtml, render_qtml_styles};

let page = render_qtml!("ui/dashboard.qtml");
let css = render_qtml_styles!("ui/dashboard.qtml");

Format source with qtml_core before writing it back to disk:

use qtml_core::compiler::format_document;

let formatted = format_document(source)?;
Column {
    spacing: 0.75rem
    horizontal_alignment: center

    Text {
        text: "Welcome, {user_name}"
        size: 1.5rem
        weight: 700
    }

    Button {
        text: "Save"
        background: #2563eb
        color: white
        padding: 0.75rem
        border_radius: 0.375rem
    }
}

Imports always name the component or token namespace they introduce. QTML classifies an imported module from its declarations:

import "components/page_card.qtml" as PageCard
import "styles/theme.qtml" as theme

Imports are local to the file that declares them. Components and token namespaces must be imported in every file that uses them; imports are not re-exported to parents or siblings.

Imported components declare their public parameters with param. $ reads a dynamic Rust value or component parameter:

param title: "Untitled"

Box {
    Text { text: $title }
}

Transitions use normal CSS values:

Button {
    transition: all
    duration: 100ms
}

Use parenthesized Rust boolean expressions to conditionally render QTML content. Parentheses mark the Rust expression boundary; branches support else if and else:

if (user.is_admin()) {
    Text { text: "Admin access" }
} else if (user.is_signed_in()) {
    Text { text: "Standard access" }
} else {
    Text { text: "Sign in" }
}

Use // for comments. Comments may appear anywhere whitespace is allowed:

// A reusable page heading.
Header1 {
    text: "Dashboard" // Rendered as escaped text.
}

For page structure, QTML provides Header, Footer, Section, Article, Nav, Aside, Text, and Span alongside Page, Box, Grid, Column, and Row. Use OrderedList, UnorderedList, and ListItem for lists; use Details and Summary for disclosures. Heading components run from Header1 through Header6.

Style modules use the same .qtml extension. They can combine reusable design tokens, dark token overrides, and one project-level defaults block:

// ui/styles/theme.qtml
token background: #f8fafc
token foreground: #0f172a

dark {
    token background: #0f172a
    token foreground: #f8fafc
}

defaults {
    Page {
        background: self.background
        color: self.foreground
    }

    Button {
        border_radius: 0.5rem
        padding_x: 1rem
        padding_y: 0.625rem
    }
}

Templates import style modules with the same syntax as component modules. Qualified names are token references, while quoted dotted values remain literals. Inside a style module's defaults, self.* references tokens declared in that same module:

import "styles/theme.qtml" as theme

Page { background: theme.background }

Generate component rules and imported token definitions once in the application shell with render_qtml_styles!("ui/home.qtml"). Imports make tokens available but do not activate imported defaults. Activate a style module's defaults by listing it directly; those defaults then apply globally throughout the generated stylesheet:

const APP_CSS: &str = render_qtml_styles!(
    "ui/styles/theme.qtml",
    "ui/home.qtml",
    "ui/settings.qtml",
);

QTML emits its built-in defaults first, directly listed project defaults next, and explicit component styles last. When multiple directly listed style modules set the same default, later macro arguments win. Imported style modules still emit required token definitions, but their defaults remain inactive unless they are also listed directly. Dark overrides set CSS custom properties under the dark class. Use a :dark { ... } style scope for local dark-mode declarations.

Prefix component style scopes with :. Named scopes accept an identifier, and selector scopes use a bracketed selector. & represents the generated component class and _ represents a descendant space:

Article {
    :hover {
        background: #2563eb
    }

    :[&_h1] {
        size: 2.25rem
    }
}

Style values are static CSS values or token references. Use brackets for values that contain spaces or punctuation. A parenthesized Rust condition may select between two static values or token references:

Button {
    width: [calc(100% - 2rem)]
    background: [linear-gradient(to right, #06b6d4, #3b82f6)]
    color: (is_active) ? white : gray
}

Unscoped styles are desktop-first. Use :tablet for 768px through 1023px, :mobile below 768px, and :desktop for an explicit 1024px-and-up rule.

QTML emits the false branch in the component's base class and emits the true branch as a conditionally rendered modifier class. Each conditional property is evaluated independently. Dynamic Rust values remain available for content and attributes, but unrestricted dynamic values are not supported as style values.

See examples/counter, examples/task_app, and examples/theme for working applications.

CSS generation

render_qtml! assigns every built-in a stable class formed by lowercasing its component name, such as q-button, q-column, or q-header1. These classes are safe hooks for CSS, JavaScript, and tests. Styled components also receive their deterministic generated classes. Grid, Column, and Row receive their built-in layout behavior from their stable classes, so they do not need generated classes unless they have explicit QTML styles. render_qtml_styles! includes QTML's clean element defaults, follows the same template and component imports, deduplicates identical rule sets, and returns the complete CSS as a compile-time string:

const QTML_STYLES: &str = render_qtml_styles!("ui/home.qtml");

Include that string once in the application shell:

<style>{QTML_STYLES}</style>

The built-in defaults provide a responsive system-font baseline, predictable box sizing, typography, links, form controls, tables, code blocks, accessible focus states, and a .dark color palette. They use low-specificity :where(...) selectors, so directly listed project defaults override them, and styles declared on components take precedence over both. Base project defaults use zero-specificity :where(...) selectors; scoped defaults such as :hover use normal selector specificity so they can override a component's explicit base style, while an explicit component :hover style still wins. The default palette can also be adjusted by overriding the --qtml-default-* custom properties.

License

QTML is available under the MIT License.