FmgLib.MauiMarkup

Visual States

The Visual State Manager changes a control's appearance based on its state — Normal, Focused, Disabled, PointerOver, Pressed, and so on. FmgLib.MauiMarkup wraps it with the strongly-typed VisualState<T> class, which plugs into styles or directly onto controls, and even supports state-entry animations.

Defining Visual States#

A VisualState<T> takes the state name and a setters lambda — the same fluent property API used everywhere else:

C#
new VisualState<Button>(VisualStates.Button.Normal, e => e
    .TextColor(Colors.White)
    .BackgroundColor(AppColors.Primary))

Built-in state names — the VisualStates helper#

Instead of magic strings, use the constants class shipped with the library:

Class Constants
VisualStates.VisualElement Normal, Disabled, Focused, PointerOver
VisualStates.Button + Pressed
VisualStates.ImageButton + Pressed
VisualStates.Switch + On, Off
VisualStates.RadioButton + Checked, Unchecked
VisualStates.CheckBox + IsChecked
VisualStates.CollectionView + Selected
VisualStates.CarouselView + DefaultItem, CurrentItem, PreviousItem, NextItem

(Each control class inherits the common VisualElement states, so VisualStates.Button.Focused is also valid.)

Visual States in a Style#

The most common placement — inside a Style<T> collection initializer, applying app-wide:

C#
new Style<Button>(e => e
    .FontSize(14)
    .CornerRadius(8))
{
    new VisualState<Button>(VisualStates.Button.Normal, e => e
        .TextColor(e => e.OnLight(Colors.White).OnDark(AppColors.Primary))
        .BackgroundColor(e => e.OnLight(AppColors.Primary).OnDark(Colors.White))),

    new VisualState<Button>(VisualStates.Button.PointerOver, e => e
        .BackgroundColor(AppColors.PrimaryDark)),

    new VisualState<Button>(VisualStates.Button.Disabled, e => e
        .TextColor(e => e.OnLight(AppColors.Gray950).OnDark(AppColors.Gray200))
        .BackgroundColor(e => e.OnLight(AppColors.Gray200).OnDark(AppColors.Gray600))),
}

Always define Normal. The VSM only restores properties that some state sets; defining Normal explicitly guarantees a clean return from other states.

Visual States Directly on a Control#

Use the VisualStateGroups attached-property method:

C#
new Entry()
    .Placeholder("E-mail")
    .VisualStateGroups(
        new VisualStateGroupList
        {
            new VisualState<Entry>(VisualStates.VisualElement.Normal, e => e
                .BackgroundColor(Colors.White)),
            new VisualState<Entry>(VisualStates.VisualElement.Focused, e => e
                .BackgroundColor(Colors.LightYellow)),
        })

VisualStateGroups takes a VisualStateGroupList, and states written straight into it land in the CommonStates group. Add a VisualStateGroup explicitly when you need a group of your own:

C#
new Grid()
    .VisualStateGroups(
        new VisualStateGroupList
        {
            new VisualStateGroup()
                .Name("SelectionStates")
                .States(
                    new VisualState<Grid>("Unselected", e => e.BackgroundColor(Colors.White)),
                    new VisualState<Grid>("Selected", e => e.BackgroundColor(Colors.LightBlue)))
        })

VisualStateGroup itself does not support collection-initializer syntax — it holds its states in a States property rather than implementing IEnumerable. Use VisualStateGroupList as above, or the fluent .States(...) method.

Animations Inside Visual States#

VisualState<T> accepts Action<T> entries in its collection initializer — they run when the state is entered, so async MAUI animations become state transitions:

C#
new Style<Button>(e => e.FontSize(20))
{
    new VisualState<Button>(VisualStates.Button.Normal, e => e
        .FontSize(33)
        .TextColor(AppColors.Gray200))
    {
        async button => {
            await button.RotateTo(0);     // animate on entering Normal
        }
    },

    new VisualState<Button>(VisualStates.Button.Disabled, e => e
        .FontSize(20)
        .TextColor(AppColors.Gray600))
    {
        async button => {
            await button.RotateTo(180);   // animate on entering Disabled
        }
    },
}

Combine with the library's generated Animate…To helpers for property-level animations:

C#
new VisualState<Button>(VisualStates.Button.PointerOver)
{
    async b => await b.AnimateBackgroundColorTo(Colors.DarkSlateBlue, length: 150)
}

State Triggers — states driven by conditions#

A VisualState<T> can also contain state triggers instead of being driven by control interaction. This enables responsive/adaptive layouts:

C#
new VisualStateGroupList
{
    new VisualState<Grid>("Wide", e => e.BackgroundColor(Colors.White))
    {
        new AdaptiveTrigger().MinWindowWidth(800)
    },
    new VisualState<Grid>("Narrow", e => e.BackgroundColor(Colors.WhiteSmoke))
    {
        new AdaptiveTrigger().MinWindowWidth(0)
    },
}

Available fluent-enabled state triggers:

Trigger Activates when
AdaptiveTrigger Window size crosses MinWindowWidth/MinWindowHeight
CompareStateTrigger A bound Property equals Value
DeviceStateTrigger Running on a given Device (platform)
OrientationStateTrigger Device orientation matches
StateTrigger IsActive is set (manual control)

Example — orientation-dependent layout:

C#
new VisualStateGroupList
{
    new VisualState<StackLayout>("Portrait", e => e.Orientation(StackOrientation.Vertical))
    {
        new OrientationStateTrigger().Orientation(DisplayOrientation.Portrait)
    },
    new VisualState<StackLayout>("Landscape", e => e.Orientation(StackOrientation.Horizontal))
    {
        new OrientationStateTrigger().Orientation(DisplayOrientation.Landscape)
    },
}

Programmatic State Changes#

Standard MAUI applies:

C#
VisualStateManager.GoToState(myButton, "CustomState");

Custom state names work fine — define a VisualState<T> with your own name and trigger it from code.

Visual States vs. Triggers#

Visual states Triggers
Driven by Named control states (+ state triggers) Property values / bindings / events
Mutually exclusive Yes, within a group No
Animation support Yes (action entries) Via EventTrigger actions
Best for Interaction feedback, adaptive layout Data-driven property changes