Skip to content

Theme Builder and Quasar color field

The Theme Builder centralizes the visual identity of forms edited with FormBuilder and rendered with FormViewer.

It replaces scattered styles with a serializable design-token contract covering palette, typography, spacing, radii, control variant, density, background, depth, motion, and iconography.

Open the Theme Builder

The Theme button in the top toolbar opens the side panel. Five presets are included:

  • Enterprise;
  • Ocean;
  • Slate;
  • Emerald;
  • Sunset.

Any manual change switches the theme to custom. The panel preview updates immediately.

The Typography tab uses a dedicated responsive grid: base size and label weight remain aligned in two columns when space allows, then collapse to one column. Line height displays its value in a fixed badge so slider labels cannot overlap the preceding controls.

Advanced customization in 0.1.12

The panel adds two domains:

  • Effects: neutral background, linear or radial gradient, angle, border, elevation, backdrop blur, and entry motion;
  • Icons: Quasar or Iconify provider, primary, secondary and destructive actions, submit, previous and next glyphs, plus icon size.

The preview immediately replays fade, slide-up, scale, and soft-pulse. The system prefers-reduced-motion preference automatically disables them.

UnoCSS acts as the Theme Builder utility layer: presetWind3 provides visual shortcuts and UI animations, while presetIcons renders Iconify glyphs. The MDI Iconify collection is bundled locally through @iconify-json/mdi; runtime rendering does not require a remote API.

Icon selection uses QIconPicker. It only receives the layer-owned curated catalog, with search, pagination, keyboard access, and Quasar QTooltip support. The dependency is intentionally pinned to stable 2.0.7, compatible with Vue 3 / Quasar 2; its v2 contract (model-pagination, itemsPerPage, and the icon slot) is protected by project static checks.

Control the theme from the host application

vue
<script setup lang="ts">
import type { QFormBuilderThemeConfig } from '@vevedh/qform-builder-layer/types'
import { createQFormBuilderTheme } from '@vevedh/qform-builder-layer/theme'

const schema = ref([])
const settings = ref({
  preview: { width: 768, isFullWidth: false },
  previewMode: 'editing' as const,
  columns: 'sm' as const,
  theme: createQFormBuilderTheme('enterprise'),
})
const theme = ref<QFormBuilderThemeConfig>(settings.value.theme)
const themeOpen = ref(false)
</script>

<template>
  <FormBuilder
    v-model:schema="schema"
    v-model:settings="settings"
    v-model:theme="theme"
    v-model:theme-open="themeOpen"
  />
</template>

The theme is also included in settings and in the save() payload. v-model:theme is a direct model for applications that persist or edit the theme separately.

Render the same theme with FormViewer

vue
<FormViewer
  v-model="values"
  :form-fields="schema"
  :theme="settings.theme"
/>

Builder and viewer use the same normalizer and CSS variables, so a saved form keeps its visual identity outside the administration page that created it.

Public API

ts
import {
  applyQFormBuilderThemePreset,
  createQFormBuilderTheme,
  mergeQFormBuilderTheme,
  normalizeQFormBuilderTheme,
  qFormBuilderThemeToCssVariables,
} from '@vevedh/qform-builder-layer/theme'

Methods exposed by the FormBuilder reference:

ts
builderRef.value?.setTheme({
  preset: 'custom',
  palette: { primary: '#0057b8' },
})

const currentTheme = builderRef.value?.getTheme()
builderRef.value?.openTheme()
builderRef.value?.closeTheme()

Design tokens

QFormBuilderThemeConfig covers palette, typography, spacing, radii, input variant, density, and container shadow. Tokens are scoped under .qform-theme; they do not globally overwrite the host application's Quasar palette.

New q-color field

The Community catalog no longer relies on a browser-dependent QInput type="color". The new FormKit q-color adapter combines:

  • a controlled text input;
  • a color swatch;
  • a Quasar QColor popup;
  • HEX, HEXA, RGB, and RGBA formats;
  • spectrum, tune, and palette views;
  • a configurable palette;
  • optional header and footer;
  • a clearable value.
ts
const colorField = {
  $formkit: 'q-color',
  name: 'brandColor',
  label: 'Primary color',
  value: '#2563eb',
  formatModel: 'hexa',
  defaultView: 'spectrum',
  palette: ['#2563eb', '#0f766e', '#7c3aed'],
  clearable: true,
}

Existing schemas that use $formkit: 'q-input' with inputType: 'color' remain renderable for backward compatibility. They can be migrated progressively to q-color to access the advanced options.

Validation and security

The q-color field deliberately follows QColor's native contract: HEX, HEXA, RGB, and RGBA. Theme Builder design tokens use a separate CSS normalizer that also accepts HSL and HSLA. Active values such as url(...), concatenated CSS declarations, and unsafe font-family strings are rejected.

This separation prevents unsupported values from being forwarded to QColor while preserving flexible CSS tokens. It protects builder rendering; themes received from a client must still be validated by the Feathers/NFZ service before persistence.

Quasar model compatibility

Layer adapters normalize models before forwarding them to Quasar components:

  • q-color receives only a QColor-compatible string or null;
  • the color text editor explicitly uses a text input;
  • q-time and q-datetime convert missing FormKit values (undefined or an empty string) to null;
  • schema attributes such as format24h, withSeconds, and nowBtn are converted to real booleans;
  • picker-only attributes are not leaked to the internal HTML control.

These rules prevent browser “value cannot be parsed” messages and Vue warnings from the QTime model validator.

Demo

The playground provides:

txt
/theme

Automatic light/dark consistency

The Theme Builder now separates the serialized theme from the effective rendered theme:

  • saved brand and status colors remain unchanged;
  • when Quasar enables dark mode, the layer derives only the surfaces, text colors and borders required for contrast;
  • neither the schema nor the persisted theme is mutated.

Reference dark palette:

ts
{
  surface: '#0f172a',
  surfaceMuted: '#1e293b',
  text: '#f8fafc',
  textMuted: '#cbd5e1',
  border: '#475569',
}

Public helpers accept an optional color-mode argument:

ts
import {
  qFormBuilderThemeInputProps,
  qFormBuilderThemeToCssVariables,
  resolveQFormBuilderThemeForColorMode,
} from '@vevedh/qform-builder-layer/theme'

const effectiveTheme = resolveQFormBuilderThemeForColorMode(savedTheme, {
  dark: true,
})

const style = qFormBuilderThemeToCssVariables(savedTheme, {
  dark: true,
})

const inputProps = qFormBuilderThemeInputProps(savedTheme, {
  dark: true,
})

FormBuilder, FormViewer, and the Theme Builder preview automatically follow useQuasar().dark.isActive; the host application only needs to manage Quasar dark mode.

Covered components

The .qform-theme scope normalizes:

  • QInput, QSelect, QFile, QColor, and date/time pickers;
  • QEditor, its toolbar, and content area;
  • checkboxes, radios, toggles, option groups, and button toggles;
  • sliders, ranges, ratings, tabs, and steppers;
  • sections, fieldsets, columns, editing overlays, and nested fields;
  • quotes, information/warning banners, and code blocks, including legacy schemas.

Static content receives semantic rendering classes without mutating source data, so forms saved with historical bg-*-1 / text-*-9 classes remain readable.

Visual audit

The playground provides:

txt
/dark-mode-audit

Associated commands:

bash
bun run dark-mode:check
bun run dark-mode:runtime:check
bun run e2e:visual:update
bun run e2e:visual

Snapshots protect light preview, dark preview, and dark editing mode. New component surfaces must consume --qfb-surface, --qfb-surface-muted, --qfb-text, --qfb-text-muted, and --qfb-border instead of hard-coded light colors.

QForm Builder — Nuxt 4, Quasar and FormKit layer for dynamic forms.