AKCSS Utilities

AKCSS Utilities

Utilities are small reusable styles that can be applied directly as markup attributes.

using Avalonia.Controls;
using Akbura.Styles.akcss;

<Button w-40 h-10 px-4 bg-blue-500 text-white rounded-md>
    Continue
</Button>

Unlike regular AKCSS classes, utilities do not use the class attribute:

<Button class="primary" w-40 />

Here:

  • primary is an AKCSS class.
  • w-40 is an AKCSS utility.

Declaring utilities

Custom utilities are declared inside an @utilities section:

@using Avalonia.Controls;

@utilities {
    Control.inactive {
        Opacity: 0.5;
        IsHitTestVisible: false;
    }
}

The utility is then used as a flag-style attribute:

<Button inactive>
    Disabled action
</Button>

A utility without parameters uses its complete selector name. Names may contain multiple segments:

@utilities {
    Control.self-start {
        HorizontalAlignment: Left;
        VerticalAlignment: Top;
    }
}
<Button self-start>
    Aligned to start
</Button>

Parameterized utilities

Parameters are declared after the utility name:

@using Avalonia.Controls;

@utilities {
    Control.square-(double size) {
        Width: size;
        Height: size;
    }
}

Pass the argument as another segment of the markup attribute:

<Button square-48>
    Square button
</Button>

In this example, 48 is converted to the double size parameter.

A utility can have multiple parameters:

@using Akbura;
@using Avalonia.Controls;
@using Avalonia.Media;

@utilities {
    Border.frame-(string color)-(int shade) {
        BorderBrush:
            Amx.DynamicResource<IBrush>(
                "--color-" + color + "-" + shade);
    }
}
<Border frame-blue-500>
    Content
</Border>

The arguments are resolved as:

color = "blue"
shade = 500

Utility parameters are typed. AKCSS supports regular C# types, including:

  • double
  • int
  • string
  • enums
  • custom types

An incompatible argument produces a compiler diagnostic.

Expression arguments

Use {...} when a utility argument comes from a C# expression:

using Avalonia.Controls;

@akcss {
    @utilities {
        Control.square-(double size) {
            Width: size;
            Height: size;
        }
    }
}

state double buttonSize = 48;

<Button square-{buttonSize}>
    Dynamic size
</Button>

Expressions are bound in the current component scope and can reference states, parameters, local variables, and other C# expressions.

state double width = 20;
state double scale = 2;

<Button square-{width * scale} />

Markup extension arguments

Use ${...} when a markup extension supplies a utility argument:

using Demo.Markup;

state double spacing = 4;

<Border p-${GalleryPadding {spacing + 1}‎} />

The extension is resolved and bound at compile time. For a utility parameter T, its ProvideValue method may return T, IObservable<T>, IObservable<object>, or BindingBase.

Observable and binding results reapply only the affected utility conflict. When an extension argument contains a C# expression, it is recreated on each component update so it sees the current state and parameter values.

See Markup Extensions for the full value and lifecycle contract.

Conditional utilities

A utility can be applied conditionally with an expression prefix:

using Avalonia.Controls;
using Akbura.Styles.akcss;

state bool isBusy = false;

<Button {isBusy}:opacity-50>
    Save
</Button>

The opacity-50 utility is applied only while isBusy is true.

Another common example is conditional visibility:

<TextBlock {isBusy}:hidden>
    Content is ready
</TextBlock>

The condition must be a valid boolean expression:

<Button {count > 10}:hidden />
<Button {user.IsAdmin}:visible />

Markup extension prefixes

A markup extension can act as a reactive utility condition:

using Akbura.Markup;
using Akbura.Styles.akcss;

<Border p-1
        ${sm}:p-2
        ${md}:p-3
        ${lg}:p-4
        ${xl}:p-5
        ${xxl}:p-6 />

The extension must return bool or IObservable<bool>. A false or not-yet- available value removes that candidate from conflict resolution, allowing the next matching utility to apply.

The braces are required. md:p-3 is retained only for parser recovery and produces a diagnostic that suggests ${md}:p-3.

Variant priority

Utilities conflict by their resolved utility name. For example, p-1, p-2, and ${md}:p-3 all use the conflict key p. Only one candidate wins for that key. Unrelated keys such as p and bg are resolved independently.

The runtime resolves active candidates in this order:

  1. For each non-empty ConflictGroup, choose the candidate with the greatest Order.
  2. If Order is equal, choose the candidate written later.
  3. Candidates from different groups, and candidates without a group, are compared by source order rather than by Order.
  4. Compare the winning prefixed candidate with the last unprefixed candidate according to UnprefixedPrecedence.

The three unprefixed precedence modes are:

Mode Result
Below The unprefixed utility always wins.
SourceOrder The candidate written later wins.
Above The active prefixed utility always wins.

Custom variants declare these values on their extension type:

using Akbura.Markup;

[UtilityVariant(
    10d,
    ConflictGroup = "WindowBreakpoints",
    UnprefixedPrecedence = UnprefixedUtilityPrecedence.Above)]
public sealed class WideExtension
{
    public IObservable<bool> ProvideValue(IServiceProvider services)
    {
        // Return an observable condition for the target.
    }
}

UtilityVariantAttribute decides priority only after utilities are known to conflict. Sharing a ConflictGroup never creates a conflict between unrelated utilities.

Built-in breakpoints

The built-in variants are available through an explicit import:

using Akbura.Markup;
Variant Active width Order
${sm} >= 640 1
${md} >= 768 10
${lg} >= 1024 20
${xl} >= 1280 30
${xxl} >= 1536 40

All built-in breakpoints belong to one conflict group and use UnprefixedUtilityPrecedence.Above. Therefore the largest active breakpoint wins even when an unprefixed utility appears later.

Conditional declarations inside utilities

Utilities may also contain reactive @if declarations:

@using Avalonia.Controls;

@utilities {
    Control.interactive {
        Opacity: 0.8;

        @if(IsPointerOver) {
            Opacity: 1;
        }
    }
}
<Button interactive>
    Hover over me
</Button>

The utility observes IsPointerOver and reevaluates when the property changes.

Declarations inside @if use BindingPriority.StyleTrigger, giving them priority over regular utility declarations using BindingPriority.Style.

Enum parameters

Enum members can be passed as utility segments:

@using Avalonia;
@using Avalonia.Controls;

@utilities {
    Control.align-(HorizontalAlignment alignment) {
        HorizontalAlignment: alignment;
    }
}
<Button align-Center />
<Button align-Right />

Enum member names are resolved using the declared parameter type.

An expression can also be used:

state HorizontalAlignment alignment =
    HorizontalAlignment.Center;

<Button align-{alignment} />

Type-specific utilities

A utility can restrict itself to a particular control type:

@using Avalonia.Controls;

@utilities {
    StackPanel.gap-(double value) {
        Spacing: value;
    }

    Grid.gap-(double value) {
        ColumnSpacing: value;
        RowSpacing: value;
    }
}

The compiler selects the utility compatible with the target control:

<StackPanel gap-12 />

<Grid gap-12 />

A type-specific utility can also be used on derived controls.

Applying it to an incompatible type produces a utility-not-found diagnostic:

<!-- StackPanel.gap cannot be applied to TextBlock -->
<TextBlock gap-12 />

Parenthesized and fully qualified types are supported:

@utilities {
    (Demo.Controls.Card).compact {
        Padding: 4;
    }

    (global::Demo.Controls.SpecialButton).wide {
        Width: 200;
    }
}

Using utilities with @apply

Utilities can be composed into a regular AKCSS class with @apply:

@using Akbura.Styles.akcss;

.card {
    @apply w-full p-4 bg-slate-100 rounded-md shadow-sm;
}
<Border class="card">
    Card content
</Border>

@apply can combine both styles and utilities:

@using Demo.Styles.Shared.akcss;
@using Akbura.Styles.akcss;

.panel {
    @apply surface w-full p-4 rounded-lg;
}

Where utilities are resolved from

When a utility is used in markup, Akbura searches in this order:

  1. Utilities declared in an inline @akcss block.
  2. Utilities from the component's companion .akcss file.
  3. Imported .akcss modules, in using order.

For example, Counter.akbura automatically sees utilities from Counter.akcss:

Counter.akbura
Counter.akcss

Shared utilities are imported explicitly:

using Demo.Styles.Utilities.akcss;
<Button custom-utility />

If the same selector is declared more than once in the same resolution layer, the compiler reports an ambiguous utility diagnostic.

Built-in utilities

Akbura provides Tailwind-inspired built-in utilities through:

using Akbura.Styles.akcss;

Common categories include:

Category Examples
Size w-10, h-8, size-12, w-full, h-auto
Min/max size min-w-10, max-h-40
Margin m-4, mx-2, mt-6, mb-4
Padding p-4, px-3, py-2, pl-4
Layout hidden, visible, self-center
Grid col-1, row-2, col-span-2
Spacing gap-4, gap-x-2, gap-y-3
Opacity opacity-50, opacity-100
Colors bg-blue-500, text-white
Typography text-lg, font-bold, text-center
Borders border-1, border-slate-300
Radius rounded-md, rounded-full
Shadows shadow, shadow-lg, shadow-2xl

Utilities are type-aware. For example:

  • gap-4 works differently for StackPanel and Grid.
  • rounded-md is available for controls supporting CornerRadius.
  • text-center is available for text controls.
  • bg-blue-500 is resolved for controls supporting Background.

Spacing scale

Built-in numeric sizing and spacing utilities use the shared --spacing resource.

The default value is 4:

w-10 → Width = 10 × 4 = 40
p-3  → Padding = 3 × 4 = 12
gap-4 → Spacing = 4 × 4 = 16

Built-in colors, radii, font sizes, weights, and shadows also use dynamic Avalonia resources:

--color-blue-500
--radius-md
--text-lg
--font-weight-bold
--shadow-lg

Because these are dynamic resources, applications can customize the theme without redefining every utility.