M
Marai.UI
v1.0.0-alpha.7

Best Practices

Guidance for writing maintainable, accessible, and Tailwind-compatible Marai.UI code.

Always Use Literal Tailwind Strings

Tailwind's scanner reads your source files at build time and includes only the utility classes it finds as literal text. Dynamic class construction — string interpolation, conditional concatenation, or classes stored in a database — will not be included in the output CSS.

Do
razor
<MButton Class="px-4 py-2 rounded bg-blue-600 text-white hover:bg-blue-700">Save</MButton>
Avoid
razor
@* Tailwind won't scan the constructed class — avoid this *@
<MButton Class="@($"px-4 py-2 rounded bg-{color}-600 text-white")">Save</MButton>

When you need conditional classes, use a ternary with both values as literals:

razor
<MButton Class="@(isDestructive ? "px-4 py-2 rounded bg-red-600 text-white" : "px-4 py-2 rounded bg-blue-600 text-white")">
    @label
</MButton>

Use Part Parameters for Sub-Element Styling

Components with multiple styled regions expose dedicated class parameters — InputClass, TrackClass, OverlayClass, etc. Pass Tailwind utilities to the specific part rather than trying to target inner elements with arbitrary variants.

razor
<MSwitch
    InputClass="peer sr-only"
    TrackClass="relative block w-11 h-6 rounded-full bg-gray-300 transition-colors peer-checked:bg-blue-600"
    @bind-Checked="enabled" />

Always Pair Inputs with Labels

Every form control needs a visible label linked by a matching id / For attribute pair. Screen readers announce the label when focus moves to the input.

razor
<MLabel For="username" Class="block text-sm font-medium text-gray-700 mb-1">
    Username
</MLabel>
<MInput id="username" Class="w-full rounded-md border px-3 py-2 text-sm" @bind-Value="username" />

When no visible label is appropriate, supply an aria-label directly on the component:

razor
"<MInput aria-label=\"Search\" Class=\"...\" placeholder=\"Search...\" />"

Group Switches in a Fieldset

When rendering a list of MSwitch or MCheckbox controls that belong to the same category, wrap them in a <fieldset> with a <legend>. Screen readers announce the legend before each control, giving users context.

razor
<fieldset>
    <legend class="text-sm font-semibold text-gray-700 mb-3">Notification preferences</legend>
    <div class="flex flex-col gap-3">
        <MSwitch InputClass="peer sr-only" TrackClass="..." @bind-Checked="emailNotifs">
            Email notifications
        </MSwitch>
        <MSwitch InputClass="peer sr-only" TrackClass="..." @bind-Checked="smsNotifs">
            SMS notifications
        </MSwitch>
    </div>
</fieldset>

Link Help Text with aria-describedby

When a form field has visible help text or an error message below it, pass the help element's id via aria-describedby so screen readers announce both the label and the description when the field is focused.

razor
<MLabel For="email" Class="block text-sm font-medium mb-1">Email</MLabel>
<MInput id="email" type="email" aria-describedby="email-hint"
    Class="w-full rounded-md border px-3 py-2 text-sm" @bind-Value="email" />
<p id="email-hint" class="mt-1 text-xs text-gray-500">We'll never share your email.</p>

Extract Repeated Class Strings

Define shared utility strings as C# constants rather than copying them across files. This keeps strings literal for the Tailwind scanner while giving you a single point of change. See Reusable Style Constants for a full example.

Accessibility Checklist

  • Every MInput, MSelect, MCheckbox, MSwitch, and MSlider is paired with an MLabel or has an aria-label
  • Interactive components (MButton, MDropdown, MDialog) are reachable by keyboard and respond to Enter / Space
  • MDialog traps focus while open — no additional implementation required
  • Color is not the only visual indicator of state — use text, icons, or shape in addition to color
  • MSpinner has an accessible label via aria-label — override it with a descriptive string when context matters (e.g., aria-label="Saving changes")
  • Tables built with MDataTable include <thead> with scope="col" automatically