Themes and modifiers

← Actions · Documentation · Next: Observers →

Theme layers

BDUI uses semantic tokens instead of product-specific hard-coded styles:

BDUI accessible default
        ↓ overridden by
host application theme (`BduiConfig`)
        ↓ overridden by
root widget theme (only when host policy permits)

Each layer deep-merges into the previous one. Missing/invalid values retain the preceding value.

Bdui.init(
    BduiConfig {
        theme = BduiTheme.default().copy(
            colors = BduiTheme.default().colors.copy(
                primary = BduiColor(
                    light = Color(0xFF0057D9),
                    dark = Color(0xFF8AB4FF),
                ),
            ),
        )

        backendThemePolicy = BduiThemePolicy(
            allowOverrides = true,
            allowedSections = setOf(
                "colors", "typography", "shapes", "spacing", "components",
            ),
            allowedFontFamilies = setOf("brand", "system"),
        )
    }
)

Tokens

Colors

Typical semantic keys:

  • primary, onPrimary
  • secondary, onSecondary
  • surface, onSurface, surfaceVariant, onSurfaceVariant
  • background, onBackground
  • outline
  • error, onError
  • success, onSuccess
{
  "theme": {
    "colors": {
      "primary": { "light": "#0057D9", "dark": "#8AB4FF" },
      "surface": { "light": "#FFFFFF", "dark": "#121212" }
    }
  }
}

Colors accept #RRGGBB and #AARRGGBB. Which side of each token is used comes from root state.theme: "light" or "dark". If that key is missing, the host system appearance is used.

{
  "state": { "theme": "light" },
  "theme": {
    "colors": {
      "onSurface": { "light": "#111111", "dark": "#F4F4F5" }
    }
  }
}

Patch $root/state/theme to "dark" to switch appearance.

Typography

Typical keys:

  • displayLarge, displayMedium, displaySmall
  • headlineLarge, headlineMedium, headlineSmall
  • titleLarge, titleMedium, titleSmall
  • bodyLarge, bodyMedium, bodySmall
  • labelLarge, labelMedium, labelSmall

Each token may contain host-allowed fontFamily, fontSize, fontWeight, lineHeight, and letterSpacing.

{
  "theme": {
    "typography": {
      "titleLarge": {
        "fontFamily": "brand",
        "fontSize": 22,
        "fontWeight": 600,
        "lineHeight": 28
      }
    }
  }
}

Backend JSON cannot supply executable/downloadable fonts. Font family names resolve only to resources registered by the host. User font scale always applies.

Shapes

Typical keys: extraSmall, small, medium, large, extraLarge.

{
  "theme": {
    "shapes": {
      "small": { "cornerRadius": 8 },
      "medium": { "cornerRadius": 12 }
    }
  }
}

Spacing

Semantic spacing avoids repeating product constants:

{
  "theme": {
    "spacing": {
      "xs": 4,
      "sm": 8,
      "md": 16,
      "lg": 24,
      "xl": 32
    }
  }
}

Use as compatible paths:

{
  "name": "paddingHorizontal",
  "value": "$theme.spacing.md"
}

Components and variants

Component tokens define coherent variants:

WidgetDefaultExamples
Buttonprimarysecondary, tertiary, danger
Carddefaultoutlined, elevated
TextFieldoutlinedfilled
Chipfilterassist, input
TextbodyMediumtypography token names
{
  "theme": {
    "components": {
      "Button": {
        "primary": {
          "containerColor": "$theme.colors.primary",
          "contentColor": "$theme.colors.onPrimary",
          "shape": "$theme.shapes.medium"
        },
        "danger": {
          "containerColor": "$theme.colors.error",
          "contentColor": "$theme.colors.onError"
        }
      }
    }
  }
}

Resolution:

  1. explicit widget param;
  2. selected $theme.components.<Widget>.<variant>;
  3. BDUI widget default.

Widgets derive enabled, pressed, focused, selected, and error states from component state tokens. The backend does not manually send per-interaction colors.

Using theme paths

Theme and state paths are separate:

{
  "type": "Text",
  "params": {
    "text": "$root.title",
    "color": "$theme.colors.primary",
    "fontSize": "$theme.typography.titleLarge.fontSize"
  }
}

An unresolved theme token falls back to the previous layer or the widget default.

Backend safety policy

The host controls:

  • whether backend overrides are allowed;
  • allowed sections/tokens;
  • registered font family names;
  • color, size, radius, spacing, and typography bounds;
  • URL/asset policy where component tokens can refer to assets.

Reject and log:

  • malformed/non-finite values;
  • negative dimensions;
  • typography outside configured bounds;
  • unknown font family;
  • token-reference cycles;
  • unapproved sections;
  • remote executable assets.

Do not fail rendering or trigger action error observers for rejected style values.

Theme overrides

patch updates the backend theme layer on $root/theme. merge adds/overrides tokens; replace replaces the backend layer. Host BduiThemePolicy filters sections/tokens. Rejected tokens are logged and dropped; the patch still succeeds.

{
  "type": "patch",
  "patch": [
    {
      "op": "merge",
      "path": "$root/theme",
      "value": {
        "colors": {
          "primary": { "light": "#0057D9", "dark": "#8AB4FF" }
        }
      }
    }
  ]
}

See patch. A later root snapshot or patch that replaces $root may replace the root theme.

Modifiers

Envelope modifier is an ordered array applied left-to-right. It is not a param:

{
  "modifier": [
    { "name": "width", "value": "40" },
    { "name": "height", "value": "40" },
    { "name": "paddingHorizontal", "value": "16" }
  ]
}

Size values are dp, not device pixels. A 40 width is 40 density-independent pixels on every platform. Physical px is not part of the vocabulary: it would ignore screen density and break accessibility scaling.

NameValueCompose behavior
fillMaxWidthunusedfillMaxWidth()
fillMaxHeightunusedfillMaxHeight()
widthdp/pathwidth(value.dp) — exact width
heightdp/pathheight(value.dp) — exact height
minWidthdp/pathwidthIn(min = value.dp)
minHeightdp/pathheightIn(min = value.dp)
maxWidthdp/pathwidthIn(max = value.dp)
maxHeightdp/pathheightIn(max = value.dp)
paddingdp/pathpadding(value.dp) on all sides
paddingVerticaldp/pathpadding(vertical = value.dp)
paddingHorizontaldp/pathpadding(horizontal = value.dp)
paddingStartdp/pathRTL-aware start padding
paddingTopdp/pathtop padding
paddingEnddp/pathRTL-aware end padding
paddingBottomdp/pathbottom padding
backgroundcolor/path#RRGGBB / #AARRGGBB fill. Put clip before background to round it
clipdp/pathclip(RoundedCornerShape(value.dp))
weightnumber/pathRow/Column child flex; ignored elsewhere

width / height set a fixed size. Use min* / max* when the widget may grow or shrink. Combining width with fillMaxWidth is order-dependent and usually a mistake — pick one. Negative and non-finite sizes are treated as absent.

Order matters:

{
  "modifier": [
    { "name": "paddingHorizontal", "value": "16" },
    { "name": "fillMaxWidth" }
  ]
}

is not necessarily equivalent to filling first and padding second.

Unknown modifier names are ignored and logged. A missing modifier produces bare Modifier. Invalid/negative dimensions are clamped or ignored according to host policy.

← Actions · Documentation · Next: Observers →

AppSana — экраны под задачу в приложениях, которые у вас уже есть.