Skip to content

Stardew Markup Language (StarML)

StarML is the markup language used by the UI Framework. It is an HTML-like syntax based on a tree of elements, each corresponding to a view.

It shares many traits with Angular templates, particularly an enhanced set of attributes that perform data binding and other special functions.

Example

<frame layout="850px 500px"
       background={@Mods/StardewUI/Sprites/MenuBackground}
       border={@Mods/StardewUI/Sprites/MenuBorder}
       border-thickness="36, 36, 40, 36">
    <lane layout="stretch content" orientation="vertical">
        <label font="dialogue" text="Hello from StardewUI!" />
        <label text={IntroParagraph} />
        <textinput layout="200px content" text={<>FarmerName} />
        <image layout="400px 100px" sprite={@Mods/TestMod/Sprites/Hello} />
        <button text="Launch" click=|LaunchCoolFeature("now")| />
    </lane>
</frame>

Elements

As with most markup languages, StarML is built around elements. An element is:

Every StarML element corresponds to a View, whose type is decided by its tag; every attribute corresponds to a property or event on that view (except structural attributes), and child elements correspond to the view's child views.

Like HTML and XML, elements can either have an explicit closing tag, or be self-closing:

Opening/Closing Tags

<frame layout="50px 50px">
    ...
</frame>

Self-Closing Tag

<label text="Hello" />

There are no strict rules around the use of opening/closing vs. self-closing tags—any element can be written using either style. The difference is that self-closing tags cannot have any children; thus tend to be used for "simple" views such as <label /> and <image />, while open tags are used for layout views like <frame>, <lane> and <panel>.

Because the UI Framework is an abstraction over regular Views, it will not enable you to do anything with a View that would be impossible or prohibited in the Core Library, such as add children to a non-layout view.

Tags

A tag is anything between a pair of angle brackets < >, one of:

  • An opening tag: <panel>
  • A closing tag: </panel>
  • A self-closing tag: <panel />.

While the term "tag" may sometimes be used synonymously with "element", tags refer more narrowly to the specific markup above, i.e. not including any attributes or children.

In StarML, tags are not arbitrary; except for <include>, the tag defines the specific type of view that is to be created, which in turn determines what attributes are allowed and how many children it is allowed to have.

These are the standard tags available in the UI framework:

Note

The full list of tags currently supported can always be found in the ViewFactory source.

Tag
View/Behavior
Description
<banner> Banner Displays a banner, aka "scroll", using a cartoonish font. Background optional.
<button> Button Simple raised button with optional hover effect.
<checkbox> Checkbox Checkbox with optional clickable label.
<digits> Tiny Number Label Displays a number in extra-small font; used for item quantities.
<dropdown> Drop-Down List Select from a list of options.
<expander> Expander Can be clicked to show or hide more content.
<frame> Frame Draws a border and/or background around another view.
<grid> Grid Uniform grid layout using either fixed size per item or fixed number of items per row/column.
<image> Image Displays one image using a variety of scaling and fit options.
<include> Included View Insert a different StarML view in this position, using its asset name to load the content.
<keybind> Keybind Displays the buttons bound for a single keybinding.
<keybind-editor> Keybind Editor Displays and allows rebinding of all button combinations in a keybind list.
<label> Label Displays single- or multi-line text using a standard SpriteFont.
<lane> Lane Arranges other views along one axis, either horizontal (left to right) or vertical (top to bottom).
<marquee> Marquee Animates scrolling text or other content horizontally; named after the HTML Marquee.
<outlet> N/A Default or named template outlet. Only valid within a <template> element.
<panel> Panel Displays all children as layers ordered by z-index. Positions can be adjusted using margins.
<scrollable> Scrollable View Shows scroll bars and arrows when content is too large to fit.
<slider> Slider A numeric slider that can be moved between a minimum and maximum range.
<spacer> Spacer Draws nothing, but takes up space in the layout; used to "push" siblings to one side.
<tab> Tab Push-down tab used to select the active section or page of a larger menu. Can be rotated for side navigation.
<template> Template Defines a custom tag for replacement. Must be at the document root; not valid as a child of any other element.
<textinput> Text Input Input box for entering text; includes on-screen keyboard when activated by gamepad.

Attributes

Tags define what view will be created; attributes define how it will look and behave, and specifically its properties and events.

An attribute is any string appearing inside a tag that has the form attr=value, where value is one of the supported flavors, such as a quoted string like text="Hello" or data binding expression like text={HelloMessage}.

There are also structural attributes which are a separate topic.

Common Attributes

Note

These are the attributes common to all types of views (tags). Specific views usually have additional properties. Refer to the standard views documentation for details.

In StarML, the name of an attribute is always the kebab-case version of the property name, e.g. HorizontalContentAlignment becomes horizontal-content-alignment.

Attribute  Direction (1) Type (2) Explanation
actual-bounds Out Bounds True outer bounds of the view relative to its parent, including margins.
border-size Out Vector2 Size of the view's content plus padding and border width.
content-bounds Out Bounds True bounds of the view's content relative to its parent, excluding margins.
content-size Out Vector2 Size of the view's content, not including padding, borders or margins.
draggable In/Out bool Allows this view to receive drag events (drag, drag-start and drag-end).
focusable In/Out bool Allows this view to receive focus, or "snap", when using a gamepad. Automatically enabled for most interactive elements like button or dropdown.
inner-size Out Vector2 Size of the view's content and padding, excluding borders and margins.
layout In/Out LayoutParameters The intended width and height. See conversions for allowed values.
margin In/Out Edges Pixel sizes for whitespace outside the border/background.
name In/Out string For <include> elements, the view's asset name; for all other elements, a user-defined name used mainly for logging and troubleshooting.
opacity In/Out float Opacity (alpha) value of the entire view, from 0.0 (fully transparent/hidden) to 1.0 (fully opaque).
outer-size Out Bounds Total layout size occupied by the element, including padding, borders and margins.
padding In/Out Edges Pixel sizes for whitespace inside the border/background.
pointer-events-enabled In/Out bool Can be set to false to prevent receiving clicks, mouseovers, etc. Use when a transparent view is drawn on top of an interactive view.
scroll-with-children In/Out Orientation Forces the entire view to be visible when navigating in the specified direction. See the ScrollableView documentation for details.
tags In/Out Tags Allows arbitrary data to be associated with the view. Not supported in StarML yet - may be supported in the future.
tooltip In/Out TooltipData Tooltip to show when hovered with the mouse, or focused on via game controller.
transform In/Out Transform Local transform to apply to the view.
transform-origin In/Out Vector2 Relative origin position for the transform; see Transform Origin.
visibility In/Out Visibility Whether to show or hide the view; hiding does not remove it from the layout, use *if for that.
z-index In/Out int Drawing order within the parent; higher indices are drawn later (on top).
  1. In/Out properties can accept any directional binding modifier; Out properties are read-only and can only be used to write to the model, e.g. if you need to receive the view's actual pixel size after a layout.

    Attempting to bind an Out property without the > modifier, or attempting to assign it a literal value with a ="value" type attribute, will cause it to fail.

  2. StardewUI's views will often have property types that can't be carried across the API boundary due to current limitations on SMAPI and its version of Pintail.

    To help you through this, automatic conversions to and from other common types, such as tuples and XNA/MonoGame structures, are often provided if you want to bind one to your model and would rather not make everything a string.

    Note that some conversions may be lossy, i.e. if there is a difference in numeric precision.

Structural Attributes

Structural attributes look like regular attributes, but with a * prefix. Instead of binding to properties or events, they control aspects of how the view tree is constructed.

Attribute  Expected Type Description
*case Any Removes the element unless the value is equal to the most recent *switch. The types of *switch and *case must either match exactly or be convertible.

Can be negated.

*context Any Changes the context that all child nodes bind to; used for heavily-nested data models.
*if bool Removes the element unless the specified condition is met.

Can be negated.

*float FloatingPosition Makes the element a floating element.
*outlet string Specifies which of the parent node's outlets will receive this node.
Does not support bindings. The attribute value must be a quoted string.
*repeat IEnumerable Repeats the element over a collection, creating a new view for every item and setting its context to that item. Applies to both regular and structural attributes; e.g. if *repeat and *if are both specified, then *repeat applies first.
*switch Any Sets the object that any subsequent *case attributes must match in order for their elements to show.

Negation

Structural attributes linked to conditional behavior can be negated, by placing a ! before the assignment. For example:

<label *!if={Condition} ... />

The above would cause the label to only be displayed when the condition is false, rather than true. Similarly, negating a *case attribute as *!case would cause it to only display its content when the value does not match.

Negation is only allowed for specific conditional attributes, such as *if and *case. Consult the structural attributes table above for the attributes that say "can be negated".

Behavior Attributes

Attributes beginning with a + character are Behaviors, which are independent entities able to act on some view, similar to the way a C# extension method acts on its target.

The behavior system is designed to be extensible through add-ons, so unlike structural attributes, there is not necessarily a universal list of valid behaviors. The table below covers the behaviors that are available in "vanilla" StardewUI, without any add-ons. Behaviors generally take an argument in addition to their attribute value, following a : separator in the attribute name.

Behavior Argument Value Type Description
hover:<arg> Any regular attribute Same as <arg> attribute Changes the <arg> property to a new value when the pointer enters the view, and reverts it when the pointer leaves the view.
show:<arg> Any regular attribute Same as <arg> attribute Changes the <arg> property to a new value when the view becomes visible, including when first created e.g. as the result of an *if; reverts it when the view is hidden.
state:<name> string bool Creates a named state whose behaviors are toggled on/off by the bound value. This is intended for use with state triggers (see below).
state:<name>:<arg> string, then any regular attribute Same as <arg> attribute Changes the <arg> property to a new value when the state with specified name becomes active, and reverts it when the state becomes inactive. Only works when state:<name> is defined on the same element.
transition:<arg> Any regular attribute Transition Applies a transition to the <arg> property when its value changes, causing it to animate gradually from its current value to the new value instead of changing immediately.

Events

Event attributes look similar to property attributes, but deal specifically with .NET events raised by views. More generally, they are one of the two ways it is possible for the UI to communicate something back to your mod (the other being output/two-way bindings).

While some UI might be purely informational (e.g. a tooltip or HUD), any interactive UI will probably involve one or more event bindings.

Bindings for events use a specific flavor, where the handler and its arguments are enclosed in a pair of pipes (|):

<image click=|PlantCrops("corn", ^Quantity, $Button)| />

To experienced C# programmers, this may look like an ordinary method call, but it isn't. Event bindings are a powerful and complex feature, and reading the documentation on them is strongly recommended before using them.

Note

The events described below are the events common to all types of views (tags). Specific views may have additional events. Refer to the standard views documentation for details.

In StarML, the name of an attribute is always the kebab-case version of the event name, e.g. LeftClick becomes left-click.

Event  Arguments Type (1) Condition
button-press ButtonEventArgs Any keyboard/gamepad button pressed.
click ClickEventArgs Any mouse/gamepad button clicked.
drag PointerEventArgs Ongoing drag operation; mouse was moved while button is still held.
drag-end PointerEventArgs End of a drag operation, mouse button was released.
drag-start PointerEventArgs First frame of a drag operation.
left-click ClickEventArgs Left mouse button or controller A was pressed.
pointer-enter PointerEventArgs Cursor just moved inside the view's bounds.
pointer-leave PointerEventArgs Cursor just moved outside the view's bounds.
pointer-move PointerMoveEventArgs Cursor moved within a view's bounds after entering.
right-click ClickEventArgs Right mouse button or controller X was pressed.
wheel WheelEventArgs Mouse wheel movement was detected.
  1. Provided here as a reference for looking up the properties in Events Source. You don't consume these types directly in your event handlers; consult the Event Docs for details on how to set up handlers.

Attribute Flavors

Regular HTML uses quoted attributes; to support the more complex behaviors where the attribute value should not be interpreted literally (as in, the exact value inside the quotes), StarML uses different "flavors" of attributes using different punctuation.

Bindings are not tokens

Those accustomed to Content Patcher Tokens may need to unlearn certain habits, because what goes on behind the scenes with StarML is far more complicated than string replacement. If you attempt to write "tokens" of the form attr="A {{value}} B", you are going to be disappointed.

Format
Meaning
attr="value" The literal (converted) value inside the quotes.
attr={PropertyName} The current value of the specified context property.
attr={@AssetName} The current content of the named asset.
attr={#TranslationKey} The translated string for a given translation key. Can be either unqualified (foo.bar) if referring to a translation in the same mod that provided the view, or qualified (authorname.ModName:foo.bar) if referring to a translation in any other mod.
attr={&templateParam} Replace with a template attribute of the same name. Only valid in a <template> element.
attr=|Handler(Arg1, ...)| Call the specified context method, with the specified arguments; only valid for event attributes.
Note

Double-braces ({{ and }}) are allowed in place of single braces, for those heavily accustomed to Content Patcher syntax and JSON tokens in general, but are not recommended due to the inconsistency with other attributes and reduced readability.

Binding Modifiers

In addition to the different attribute flavors, context binding attributes—that is, those of the form attr={PropertyName}—can use modifier prefixes to either redirect to a different context or change the direction of synchronization between context and view.

These modifiers work only with context property and event bindings; they cannot be used on literal attributes or assets.

Modifier
Example
Effect
^ {^Prop} Binds to the parent context instead of the current context. Multiple ^ characters can be appended to go farther up, e.g. ^^^Prop.
~ {~Foo.Prop} Binds to the typed ancestor instead of the current context.
< {<Prop} Specifies an input binding, where the view receives its value from the model but does not write back. This is the default behavior when no modifier is used, and can generally be omitted.
: or <: {:Prop} Specifies a one-time input binding, which is the same as an ordinary input binding except that subsequent changes to the value will be ignored.
> {>Prop} Specifies an output binding, where the view writes its value to the model but does not read back.
<> {<>Prop} Specifies an in/out binding, where the view both receives its value from the model and writes back to the model.

Tip

Context modifiers can be combined with direction modifiers, but order matters; the direction must come first. You can write {<>^^Prop} or {>~Foo.Prop}, but not {^^<>Prop} or {~>Foo.Prop}.

Type Conversions

In the SMAPI world, integrations between mods, including framework mods, are accomplished by duck typing, specifically through the Pintail library. This is a highly effective system for backward-compatibility and in some cases forward-compatibility; however, it presents many challenges for a UI library that uses many complex types.

Many attribute types like Bounds and Edges simply can't be transmitted across the API boundary, even if you copy their definitions. Interface types such as IView are far too complex to copy over. StardewUI doesn't want you to have to deal with these issues on a one-off basis. Instead, it has a highly integrated system of type conversions: regardless of the actual, real type of a view's property or event argument, it can be assigned or bound to any property with a convertible type.

In the table below, the String Format is what you can put in a literal attribute or a string typed context property; Converts From can be used with input bindings and Converts To can be used with output bindings or event arguments. For two-way bindings, the type must either be string or be in both the "from" and "to" lists.

Note

All primitive types (numeric and bool) can be converted from their string representation, and are not shown explicitly in the conversion table. All types can also be converted to a string, though whether or not the string is useful depends on its ToString() implementation.

indicates a possible loss of numeric precision.

Type String Format Converts From Converts To
Any enum (Field Name) N/A N/A
Point "x, y" N/A N/A
Vector2 "x, y" N/A N/A
Rectangle "x, y, width, height" N/A N/A
LayoutParameters "<Length>" (1) N/A N/A
"<Width> <Height>"
"<...>[Min..]" (12)
"<...>[Min..Max]"
"<...>[..Max]"
Length "<num>px" (2) N/A N/A
"<num>%" (3)
"content" (4)
"stretch" (5)
Edges "left, right, top, bottom" Tuple<int, int, int, int> Tuple<int, int, int, int>
Vector4 Vector4
int
Point
Tuple<int, int>
Vector2
Tuple<Point, Point>
Tuple<Vector2, Vector2>
Bounds N/A N/A Tuple<float, float, float, float>
Tuple<Vector2, Vector2>
Vector4
Rectangle
Color "#rgb" N/A N/A
"#rgba"
"#rrggbb"
"#rrggbbaa"
Sprite (9) N/A Texture2D N/A
Tuple<Texture2D, Rectangle>
ParsedItemData
SpriteFont "dialogue" (6) N/A N/A
"small" (7)
"tiny" (8)
Transform See Transforms N/A N/A
Transition [duration] [delay] [easing] (14) N/A N/A
Visibility "Visible" bool N/A
"Hidden"
TooltipData Any string (text only) N/A
Tuple<string, string>
(title + text)
Item
ParsedItemData
GridItemLayout "count: n" (10) N/A N/A
"length: n" (11)
"length: n+"
FloatingPosition "above" Func<Vector2, Vector2, Vector2>

See offsetSelector
N/A
"below"
"before"
"after"
"<edge>; X, Y"(13)
  1. Applies the same value to both the Width and Height. See Length conversions below for what values are allowed for <Length>, <Width> or <Height>.
  2. Specifies an exact value in pixels, e.g. 100px.
  3. Percentage of the container's available width or height, e.g. 50%.
  4. As wide/tall as the content wants itself to be, up to the available container size.
  5. Use the entire width/height available, after any siblings that are not stretched.
  6. Reference to Game1.dialogueFont
  7. Reference to Game1.smallFont
  8. Reference to Game1.tinyFont
  9. Sprites can be bound to model properties, but should only be done for sprites that must be dynamic. In the majority of cases, you should use sprite assets instead.
  10. n is any positive integer; lays out the grid using n items per row/column and adjusts their size accordingly.
  11. n is any positive integer; lays out the grid using a fixed width/height of n per item, and wraps to the next row/column when reaching the end.

    If the value ends with a +, the size will be expanded so that the total space used by all rows/columns is exactly equal to the grid's layout width/height.

  12. Any Length (width, height or both) can have a range appended to it specifying the min and/or max, such as 50%[100..800], meaning "prefer 50% of container, but constrained between 100px and 800px". Use open ranges to specify only a minimum, or only a maximum.
  13. Any of the edges (above, below, before or after) can have a Vector2-compatible offset appended to it, e.g. above; 5, 8 which adds the specified offset to the computed edge position. This can be used in place of margins to add more space between floating elements and their parents.
  14. All fields are optional, but any that are present must appear in the specified order.

    • Duration and delay are numbers that end with either "s" (seconds) or "ms" (milliseconds), such as 1200ms or 1.2s.
    • Easing can be a named function, including any of the functions on easings.net, or a custom easing of the form CubicBezier: x1, y1, x2, y2. For more information on cubic béziers, refer to the CSS reference.

    Examples:

    • 300ms (defaults to linear easing, no delay)
    • 300ms EaseOutCubic
    • 300ms 50ms EaseOutCubic
    • EaseOutCubic (defaults to 1 sec, no delay)

If a type shows "N/A" for conversions, that means no conversion is available, either because it is not meant to be used in that scenario, or because it is already a shared type. Shared types such as any of the XNA/MonoGame types can be used directly in your model and therefore don't require any conversions, except from string to be used in literal attributes.

Duck Typing

If a particular type conversion is not in the table above, it may be available for automatic implicit conversion. See the page on duck typing for rules and additional information on when and how this occurs.

Children

In StarML—as in HTML or XML—an element's children are any tags appearing between the parent element's opening and closing tags:

Example

<lane>
    <label text="Title" />
    <frame>
        <label text="Content" />
    </frame>
</lane>

In the above example:

  • <label text="Title" /> and <frame> are children of the <lane>
  • <label text="Content" is a child of the <frame>

In general, only layout views can have children; attempting to add children to any other view type will cause an error.

Children can only be added to elements with separate opening and closing tags, e.g. <lane>...</lane>. Any self-closing tag, even if it corresponds to a layout view, cannot contain children, because a self-closing tag cannot be paired with a regular closing tag; just as with HTML or XML, <lane/>...</lane> is simply invalid StarML and will fail to parse.

Because of these constraints, all documentation and examples on this site use opening/closing tags for layout views, and self-closing tags for other views. While this is not a requirement for valid StarML, it is recommended that you do the same in order to avoid confusion and lower the chances of creating invalid markup.

Child Limits

Some layout views can have only one child, for example frames and scrollables. That means the following is invalid markup:

Failure

<frame>
    <label text="Item 1" />
    <label text="Item 2" />
</frame>

This will parse, but will either fail to display at all or fail to display correctly, because the actual frame view only has a single Content view, not a list of views like a lane or panel.

However, this rule applies only to the constructed view, not the markup itself. If only one of the children can actually display at a time, then there is no problem.

Risky

<frame>
    <label *if={Item1Visible} text="Item 1" />
    <label *if={Item2Visible} text="Item 2" />
</frame>

This is a "maybe" because, while you might personally know – or expect – that Item1Visible and Item2Visible cannot both be true at the same time, the framework itself does not know that and cannot enforce it, and doing it this way could cause it to fail when you least expect it, e.g. long after your mod has been released and been downloaded several times.

A better way is to use *switch:

Success

<frame *switch={VisibleItem}>
    <label *case="Item1" text="Item 1" />
    <label *case="Item2" text="Item 2" />
</frame>

This version cannot fail because VisibleItem cannot be both Item1 and Item2 at the same time. In other words, it is always OK to have multiple child nodes underneath a single-view layout, if all of those nodes have a distinct *case. Otherwise, there is the possibility of failure.

Outlets

Children are grouped into "outlets", which represent specific areas or subcomponents of a layout.

Usually, this is invisible to you, as a layout view only performs one type of layout and therefore only has one child or collection of children. However, there are a few exceptions; one of them is the Expander, which allows specifying both a header view (the part that is always shown) and the content view (the part that can collapse).

To solve for these problems, StarML supports the *outlet structural attribute, which allows targeting a specific outlet with a specific element:

<expander>
    <button *outlet="header" text={ExpandCollapseText} />
    <label layout="stretch content" text={LongContent} />
</expander>

Outlet names are determined by the view itself, via the OutletAttribute being applied to specific properties. In the case of Expander, there is one named outlet for the Header property, named "header". Custom views can create named outlets using the same attribute.

If an *outlet is not specified in the markup, then the default (unnamed) outlet is assumed. When multiple outlets are available, the child limits still apply per-outlet; if any given outlet, default or named, only allows a single view, then attempting to assign multiple views to that outlet would be invalid. For example, the following would not be allowed for an <expander> element:

Broken

<expander>
    <label *outlet="header" text="Hello" />
    <label *outlet="header" text="World" />
    <label layout="stretch content" text={LongContent} />
</expander>

This is not allowed because the expander's header outlet requires a single content view. However, if this were to use an *if* condition then it would be valid again:

Success

<expander>
    <label *outlet="header" *if={IsCollapsed} text="Show help" />
    <label *outlet="header" *if={IsExpanded} text="Hide help" />
    <label layout="stretch content" text={LongContent} />
</expander>

If there is a real need to have multiple views in the outlet, then this can also be achieved using a single layout view to hold them:

Success

<expander>
    <lane *outlet="header">
        <label text="Hello" />
        <label text="World" />
    </lane>
    <label layout="stretch content" text={LongContent} />
</expander>

Comments

HTML-style block comments may be used in StarML documents, using the same syntax.

<!-- begins a comment and --> ends a comment; the content of a comment can be a single line or multiple lines.

Example

<--! My Menu -->
<lane orientation="vertical" horizontal-content-alignment="middle">
    <banner text="Menu Title" />

    <!-- Nav Bar -->
    <lane layout="200px content">...</lane>

    <!-- Main Content -->
    <frame>...</frame>

    <!--
        Spacer is added at the end of the lane to re-center the
        main content after having it pushed right by the nav bar.
    -->
    <spacer layout="200px 0px" />
</lane>

Unlike code comments, StarML comments will be visible to users of your mod if they open your .sml directly, since those files are simply ordinary content files. However, they are ignored after parsing and are not shown on screen or held in memory.

Why not HTML?

Some web frameworks, like Vue, are a subset of HTML; their templates are syntactically valid HTML, constructed in such a way that any special behavior can be understood using special tags, attributes with unusual but valid prefixes like :class, and so on. So, why not simply pull in an HTML (or XHTML) parser and use that?

This went through careful consideration but ultimately seemed to have more negatives than positives. The potential positives:

  • Several preexisting parsers are available for .NET;
  • Common editors (Visual Studio Code, Notepad++, etc.) provide built-in syntax highlighting and validation;
  • Already familiar to anyone with web development experience.

The negatives:

  • Using a third-party parser (or any third-party library) in a SMAPI environment is risky, and most parsers are not totally optimized for memory and speed.
  • Common editors provide built-in syntax highlighting and validation that could be misleading given the real constraints of a Stardew UI. For example, interpolations like attr="{value1} {value2}" are valid HTML but not actually supported.
  • Prior web development experience only helps through the shallowest part of the learning curve, not with binding/event attributes, model design, asset organization, etc.
  • If all attributes are quoted, then character escaping becomes a more significant issue, especially for literal expressions; imagine documents full of
  • An HTML parser only breaks the raw text down into stringly-typed elements and attributes; StardewUI would then need to do a second round of parsing (i.e. twice as much work) on all the tags and attribute values to determine which attributes are standard vs. structural, which values are literals vs. bindings vs. events, determine all the property names and types involved in an event handler, and so on.
  • The actual runtime model (widget tree) is not comparable to an HTML DOM; does not understand unknown tags or standard HTML tags, does not distinguish between block/inline/other styles, and so on. As an abstraction over native UI, the inner workings are more like Qt or Android than a web browser; therefore, an "it's just HTML" approach would eventually end up causing more friction and confusion than a custom language.

What about XML?

An alternative would be something similar to XAML, but these have a way of getting out of control; consider, for example, a "simple" binding redirect in XAML:

<Label Content="{Binding Path=DisplayName, RelativeSource={RelativeSource Mode=FindAncestor AncestorLevel=1}}"/>
<Label Content="{Binding Path=DisplayName, RelativeSource={RelativeSource Mode=FindAncestor AncestorType={x:Type Foo}}}"/>

Compare to a StarML context redirect:

<label text={^DisplayName} />
<label text={~Foo.DisplayName} />

And this is without any binding modifiers which add even more noise to XAML.

This is not intended as a slight against XAML, which is actually a very powerful and mature tool for cross-platform development and far better than most of the available alternatives. However, it reflects a different use case: backing an enormous framework (WPF) that is difficult to update once released, needs to scale to almost any imaginable UI scenario, and can expect a very high level of expertise from dev-users.

It is precisely because XML is so extensible that XML-based languages become convoluted over time; they encourage a way of thinking wherein new features are designed to fit into the XML structure somehow, even if the existing syntax is already confusing, verbose or requires some separately-interpreted micro-grammar, instead of the alternative of making a tiny change to the parser to support a relatively simple addition, like the <> or ^ modifiers above.

StardewUI is a mod, which means it is easy to change the parser, easy to roll out said updates, and has fewer users and UI scenarios to cover, so more effort can go into making easy things easy, instead of making nearly-impossible things possible. For scenarios that StarML can't cover, the Core Library is the escape hatch.

Why not JSON?

In many ways, JSON is the lingua franca of Stardew modding; it is the notation used by every content pack, not only for Content Patcher but for every other framework out there. Why should StardewUI be different?

While sharing some of the same concerns as HTML, such as double-parsing and character-escaping, it does route around other issues such as third-party libraries (Json.NET is technically third-party, but is already available in every SMAPI environment).

To understand why it's not a good fit, we can take a look at what kind of structure might be required for a very simple view. In this example, we display a frame border, a single header line, and the names of a list of NPCs.

<frame layout="800px content">
    <lane orientation="vertical">
        <label font="dialogue" text="Header Text" />
        <label *repeat={Npcs} text={DisplayName} />
    </lane>
</frame>
{
    "Type": "Frame",
    "Layout": "800px content",
    "Children": [
        {
            "Type": "Lane",
            "Orientation": "Vertical",
            "Children": [
                {
                    "Type": "Label",
                    "Font": "dialogue",
                    "Text": "Header Text"
                },
                {
                    "Type": "Repeat",
                    "Collection": "{{Npcs}}",
                    "Content": {
                        "Type": "Label",
                        "Text": "{{DisplayName}}"
                    }
                }
            ]
        }
    ]
}

Notice how much longer, more verbose, and more indented the JSON version is? Now picture how it would look in a view that is 5 levels deep, or 10. This is also the best case scenario for syntax, assuming we make some negative tradeoffs on the parsing side and use JObject instead of any concrete type, which is what allows us to use dictionary-style attributes for Orientation, Font and so on instead of defining yet another "Attributes": [...] array with objects inside it.

At scale, the JSON version becomes unreadable; it is difficult to look at this data and even understand what it is supposed to do in broad strokes, to say nothing of keeping track of which indentation level you're at, what the context is at that level, etc. JSON can be used to represent a syntax tree, but it is not very good at it (or any other tree).

JSON is native to the web; the "J" stands for "JavaScript", and yet it is not used by any mainstream web frameworks for defining the UI. This is because, while JSON is a good format for data storage, a full user interface cannot be represented very efficiently as plain data; it is not what we call ergonomic. That is exactly the problem that markup languages like HTML evolved to solve, and why we see similar mechanisms continue to evolve organically in every domain from .NET to Qt to Rust.