Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

QuickMaterial

Material 3 tokens for QML — colour roles, the type scale, shape, spacing, density and state layers, as a drop-in module.

import QuickMaterial

Rectangle {
    color: Colours.surfaceContainer
    radius: Corner.large

    Text {
        text: "Sign in"
        font: Type.titleMedium
        color: Colours.on.surface
    }
}

Roles, not colours. Nothing here is named for what it looks like, so a scheme can be swapped underneath and every call site stays correct. That is the whole point of a design system, and it stops working the moment anything reaches for a literal.

Requirements

  • Qt 6.6 or newer, qt6-declarative
  • quickmotion 0.3.0 or newer — StateLayer and LoadingIndicator are built on it. The six token singletons work without it; those two components will not load.

Install

git clone https://github.com/Neftedollar/quickmaterial
cd quickmaterial
sudo ./install.sh        # or ./install.sh --user, no root

Goes into Qt's QML import path — asked from Qt rather than assumed, so it lands correctly on distributions that do not use Arch's layout. DESTDIR, PREFIX and QMLDIR are honoured for packaging, and packaging/PKGBUILD builds an Arch package.

Colour

Twenty-odd roles, in five groups: primary, secondary and tertiary for accents; error; and the surface ladder that carries elevation.

Elevation in Material 3 is tonal rather than a shadow — a raised surface is a different container role, not the same colour with a shadow under it. surfaceContainerLowest through surfaceContainerHighest are that scale.

Colours.primary
Colours.surfaceContainerHigh
Colours.on.surface
Colours.on.primaryContainer

Foreground roles live under on rather than being spelled onSurface, because QML parses an identifier of the form on + Uppercase as a signal handler and a property called onSurface will not compile at all.

Themes

Themes.use("baseline")     // or "slate"
Colours.mode = "light"     // switches instantly, no reload

Two schemes ship: baseline is the Material 3 baseline in both modes, slate is a cool dark one. A scheme carrying both modes is what lets mode switch without re-reading anything.

External schemes are somebody else's job to read — hand the parsed object to applyScheme:

Colours.applyScheme(JSON.parse(text))

Not an oversight. QML has no filesystem watcher, and Qt refuses to read local files over XMLHttpRequest unless QML_XHR_ALLOW_FILE_READ is set in the environment — which a library is in no position to demand of everything that uses it. An application with a file watcher gets live reloading for free this way; Quickshell's FileView does both in one property.

Two shapes are accepted: { light: {…}, dark: {…} }, and the flat { mode: "dark", colours: {…} } that generators and caelestia write. Foreground roles keep their flat onSurface spelling in JSON — the nesting is this library's concern, not the file format's. inverseOnSurface is accepted alongside onInverseSurface, since that is what generators actually emit.

Values must be #RRGGBB or #AARRGGBB. Anything else is rejected with a warning naming the role, and the role keeps its previous value. Qt does not throw on an unparseable colour — it silently yields black — so a scheme in the wrong dialect would otherwise produce a black interface reported as a success.

Type

Fifteen styles in five roles, assigned as a whole font rather than a size:

Text {
    font: Type.bodyLarge
    lineHeightMode: Text.FixedHeight
    lineHeight: Type.line.bodyLarge
}

A size on its own is not a type style. Weight, tracking and line height travel with it, and picking a size while leaving the other three at their defaults is how text ends up almost right in a way nobody can name.

Line height is separate because a QML font cannot carry one. Set it only where text actually wraps.

Type.family and Type.iconFamily are settable — neither Roboto nor Material Symbols is packaged everywhere, and a hard requirement on a missing font renders an interface in whatever the fallback is, which for an icon font is empty boxes.

Shape

Rectangle { radius: Corner.large }        // 16
Rectangle { radius: Corner.full(height) } // a pill

Seven steps and nothing in between: 0, 4, 8, 12, 16, 28, and full. The value of a scale is that it is short — radii picked individually drift into 9, 10, 11, 13, 14, numbers nobody chose and no two components share. Corner.snap(14) gives the nearest step, for migrating a codebase that grew its own.

Named Corner rather than Shape, because Shape is the type QtQuick.Shapes exports and a singleton of that name collides with it in both import orders — one of them silently, leaving the radius 0.

Density

Metrics.field      // 56
Metrics.density = -2
Metrics.field      // 48

Material's default sizes assume a finger: a text field is 56 tall because that is comfortable to hit without looking. On a desktop driven by a keyboard and a precise pointer that is generous, and there is a documented answer — a density scale from 0 down to −3, each step subtracting 4.

So a compact desktop interface is not a departure from the specification but a stated point on it, and saying which point is the difference between a decision and numbers that drifted. Reduce density for dense, information-rich, expert interfaces; leave it at 0 for anything glanced at, used rarely, or operated at arm's length.

Metrics.minimumTarget is 48 and does not scale with density. It is an accessibility floor, not a style: a control drawn smaller should grow its hit area rather than shrink this.

State layers

Rectangle {
    color: Colours.surfaceContainer
    radius: Corner.large

    StateLayer {
        anchors.fill: parent
        rounding: parent.radius
        colour: Colours.on.surface
        hovered: area.containsMouse
        focused: control.activeFocus
    }
}

A translucent film of the content colour laid over the component, rather than the component changing to a different colour.

The difference is not academic. Swapping surfaceContainer for surfaceContainerHigh works only where a container role is already painted; on a transparent control — a bare icon button, a list row with no background — the same swap jumps from nothing to a filled box, which is why those flash a rectangle on hover. A layer over transparency is simply a faint tint, which is what was wanted.

Only one layer shows at a time, chosen by priority: pressed, then focused, then hovered. They do not add.

Set ripple: true for a press wave from the point of contact — and pass the control's area with it, or Ripple creates its own MouseArea over the control and swallows its clicks.

Components

Colours the colour roles, and applying a scheme
Type the fifteen-style type scale
Corner the shape scale
Metrics component sizes, spacing, density
States state layer opacities
Themes the schemes that ship with the library
StateLayer hover, focus and press, the Material way
LoadingIndicator a shape that turns and changes while work runs

Used by

quickgreet, a greetd greeter, and quickask, a polkit agent.

Beta

This is a 0.1. Names may still move before 1.0. Bugs and requests welcome.

Licence

MIT. See LICENSE.

About

Material 3 tokens for QML — colour roles, type scale, shape, density and state layers. For Quickshell and any Qt app.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages