Tilgængelighed i .NET MAUI: Guide til Accessible Apps med WCAG 2.1 og EAA

Byg tilgængelige .NET MAUI-apps der opfylder WCAG 2.1 AA og EAA-kravene. Med praktiske kodeeksempler for SemanticProperties, skærmlæserunderstøttelse, farvekontrast og touch-targets.

Hvorfor tilgængelighed er afgørende for din .NET MAUI-app

Over 1 milliard mennesker verden over lever med en eller anden form for funktionsnedsættelse — det er tal fra WHO. Når man lige tænker over det, er det en helt enorm gruppe brugere, som risikerer at blive lukket ude af din app, hvis du ikke tænker tilgængelighed ind fra starten.

Og det handler ikke kun om at gøre det rigtige. Med ikrafttrædelsen af European Accessibility Act (EAA) den 28. juni 2025 er tilgængelighed nu et lovkrav for apps, der distribueres i EU. Så ja — det er noget, du skal tage alvorligt.

Den gode nyhed? .NET MAUI har faktisk ret solid understøttelse af tilgængelighed via SemanticProperties, platformspecifikke skærmlæsere og en arkitektur, der gør det realistisk at bygge tilgængelige apps fra én enkelt kodebase. I denne guide tager vi dig igennem alt det, du skal vide for at bygge .NET MAUI-apps der opfylder WCAG 2.1 AA-standarden og de nye EAA-krav.

EAA og danske lovkrav: Hvad du skal vide

European Accessibility Act (EAA) er et EU-direktiv, der pålægger virksomheder at sikre, at deres digitale produkter og tjenester er tilgængelige. I Danmark er direktivet implementeret som Lov om tilgængelighedskrav for produkter og tjenester.

Hvem er omfattet?

  • Alle virksomheder der tilbyder digitale tjenester i EU — uanset om de er baseret i EU eller ej
  • E-handel, bank- og finanstjenester, transport og telekommunikation
  • Mobile apps der er tilgængelige via app-butikker i europæiske lande

Mikroforetagelsesundtagelse: Virksomheder med færre end 10 ansatte OG en årlig omsætning under €2 millioner er undtaget fra tjenestekrav — men ikke produktkrav. Og bemærk: undtagelsen gælder ikke virksomheder udenfor EU, der betjener EU-markedet.

Sanktioner ved manglende overholdelse

Bøder kan nå op til €100.000 eller 4 % af den årlige omsætning. Det er ikke småpenge. Håndhævelsen varierer mellem EU-medlemslande, men bøderne skal ifølge direktivet være "effektive, forholdsmæssige og afskrækkende."

Den tekniske standard: EN 301 549 og WCAG 2.1

EAA refererer til den europæiske standard EN 301 549, som igen inkorporerer Web Content Accessibility Guidelines (WCAG) 2.1 niveau AA som minimumskrav. I Danmark er det besluttet ved lov, at alle hjemmesider og apps skal leve op til WCAG 2.1 AA — og det er denne standard, vi fokuserer på i resten af guiden.

WCAG 2.1 og POUR-principperne

WCAG 2.1 er bygget op omkring fire grundlæggende principper, som du nok vil støde på under forkortelsen POUR:

  1. Perceivable (Mulig at opfatte): Brugere skal kunne opfatte information og UI-komponenter — f.eks. via tekstalternativer til billeder
  2. Operable (Mulig at betjene): Brugere skal kunne interagere med grænsefladen via forskellige metoder — tastatur, stemme, touch
  3. Understandable (Forståelig): Information og betjening skal være forudsigelig og letforståelig
  4. Robust: Indhold skal kunne tolkes pålideligt af hjælpeteknologier som skærmlæsere

Disse fire principper er egentlig ret intuitive, når man først har forstået dem. Tænk på det som: kan brugeren se det, bruge det, forstå det og stole på at det virker med hjælpeteknologi?

SemanticProperties: Fundamentet for tilgængelighed i .NET MAUI

SemanticProperties er .NET MAUIs primære tilgængelighedsmekanisme. Det er attached properties, der giver skærmlæsere information om dine UI-elementer. Microsoft anbefaler SemanticProperties som den foretrukne tilgang — de ældre AutomationProperties fra Xamarin.Forms-tiden er stadig understøttet, men regnes for legacy.

SemanticProperties.Description

Giver en kort, præcis beskrivelse af et element, som skærmlæseren læser højt. Det er nok den property, du kommer til at bruge allermest.

<!-- XAML -->
<Image Source="settings_icon.png"
       SemanticProperties.Description="Åbn indstillinger" />

<Button Text="💾"
        SemanticProperties.Description="Gem ændringer"
        Clicked="OnSaveClicked" />

<ImageButton Source="delete.png"
             SemanticProperties.Description="Slet element"
             Clicked="OnDeleteClicked" />

Vigtigt: Undgå at sætte Description på et Label-element — det vil faktisk forhindre, at skærmlæseren automatisk læser Text-egenskaben. Labels tekst bliver nemlig som standard læst højt af skærmlæseren, og en Description overskriver den adfærd.

// C# code-behind
Label statusLabel = new Label { Text = "Aktiv" };
Switch modeSwitch = new Switch();
SemanticProperties.SetDescription(modeSwitch, "Skift mellem aktiv og inaktiv tilstand");

SemanticProperties.Hint

Hint giver yderligere kontekst ud over beskrivelsen. Typisk bruger man den til at forklare, hvad der sker, når brugeren interagerer med elementet.

<Entry Placeholder="Indtast dit navn"
       SemanticProperties.Hint="Skriv dit fulde navn her for at oprette en profil" />

<Button Text="Send"
        SemanticProperties.Description="Send formular"
        SemanticProperties.Hint="Dobbelttryk for at sende formularen til godkendelse" />

Android-advarsel: Undgå at sætte DescriptionEntry eller Editor på Android — det vil forhindre TalkBack-handlinger i at fungere korrekt. Brug i stedet Placeholder eller Hint. Det her er en af de ting, der nemt kan koste dig et par timers debugging, hvis du ikke kender til det.

SemanticProperties.HeadingLevel

Markerer elementer som overskrifter, så brugere af skærmlæsere hurtigt kan navigere mellem sektioner. Det fungerer grundlæggende ligesom h1-h6 i HTML.

<Label Text="Profilindstillinger"
       SemanticProperties.HeadingLevel="Level1"
       FontSize="24"
       FontAttributes="Bold" />

<Label Text="Personlige oplysninger"
       SemanticProperties.HeadingLevel="Level2"
       FontSize="18"
       FontAttributes="Bold" />

<Label Text="Notifikationspræferencer"
       SemanticProperties.HeadingLevel="Level2"
       FontSize="18"
       FontAttributes="Bold" />

Du har niveauer fra Level1 til Level9 samt None (som er standard). Hold dig til en logisk hierarkisk struktur — spring ikke fra Level1 direkte til Level4, for det forvirrer skærmlæserbrugere.

Skærmlæsere på tværs af platforme

.NET MAUI understøtter alle tre store platformes skærmlæsere. Her er et hurtigt overblik over hver af dem.

Android: TalkBack

TalkBack aktiveres via Indstillinger → Tilgængelighed → TalkBack. Den navigerer via swipe-gestus og læser UI-elementer højt i rækkefølge. Første gang du tænder TalkBack, kan det ærligt talt virke lidt overvældende — men giv det fem minutter, så giver det mening.

iOS/macOS: VoiceOver

VoiceOver aktiveres via Indstillinger → Tilgængelighed → VoiceOver (iOS) eller Systemindstillinger → Tilgængelighed → VoiceOver (macOS). VoiceOver bruger swipe-gestus til navigation og tryk for at aktivere elementer.

Vigtig iOS-begrænsning: iOS giver kun tilgængelighedsegenskaber adgang fra forælder til barn. Hvis du sætter SemanticProperties inde i child-elementer af et element, der ikke selv er tilgængeligt, vil de simpelthen ikke blive læst. Det er en gotcha, der har fanget mange udviklere.

Windows: Narrator

Narrator startes med Windows-tast + Ctrl + Enter. Den navigerer via Tab-tast og piltaster — ret ligetil, hvis du er vant til tastaturnavigation.

Billeder og tilgængelighedstræet

Billeder er overalt i mobile apps. Men for en skærmlæser er et billede uden beskrivelse 100 % usynligt. Det er simpelthen som om, det ikke eksisterer.

<!-- Informativt billede - kræver beskrivelse -->
<Image Source="warning_triangle.png"
       SemanticProperties.Description="Advarsel: Din session udløber snart" />

<!-- Dekorativt billede - skjul fra tilgængelighedstræet -->
<Image Source="decorative_divider.png"
       AutomationProperties.IsInAccessibleTree="False" />

Tommelfingerreglen er enkel: Formidler billedet information? Så kræver det en beskrivelse. Er det rent dekorativt? Skjul det fra tilgængelighedstræet, så skærmlæseren ikke spilder brugerens tid på det.

Touch-targets: Minimumsstørrelser for trykbare elementer

Brugere med motoriske funktionsnedsættelser kan have rigtig svært ved at ramme små trykbare elementer. Det er en af de mest oversete tilgængelighedsproblemer, efter min erfaring. WCAG har heldigvis klare minimumskrav:

  • Android: Minimum 48×48 dp (density-independent pixels)
  • iOS: Minimum 44×44 points
  • WCAG 2.1 AA: Minimum 44×44 CSS-pixels for interaktive elementer
<!-- Sikr tilstrækkelig touch-target størrelse -->
<Button Text="OK"
        WidthRequest="48"
        HeightRequest="48"
        Padding="12" />

<!-- ImageButton med lille ikon men stor touch-target -->
<ImageButton Source="small_icon.png"
             WidthRequest="48"
             HeightRequest="48"
             Padding="12"
             BackgroundColor="Transparent"
             SemanticProperties.Description="Tilføj til favoritter" />

Farvekontrast og visuelt design

Tilstrækkelig farvekontrast er virkelig afgørende for brugere med nedsat syn — og det er faktisk noget, der også gavner alle brugere i situationer med stærkt sollys eller på dårlige skærme. WCAG 2.1 AA kræver:

  • Normal tekst: Minimum kontrastforhold 4.5:1
  • Stor tekst (18pt+ eller 14pt fed): Minimum kontrastforhold 3:1
  • UI-komponenter og grafik: Minimum kontrastforhold 3:1
<!-- Eksempel på tilgængelig farvekombination -->
<Label Text="Vigtig besked"
       TextColor="#1A1A1A"
       BackgroundColor="#FFFFFF" />
<!-- Kontrastforhold: 17.4:1 ✅ -->

<!-- Undgå lav kontrast -->
<Label Text="Svært at læse"
       TextColor="#AAAAAA"
       BackgroundColor="#FFFFFF" />
<!-- Kontrastforhold: 2.3:1 ❌ -->

Understøt dynamisk tekststørrelse

Mange brugere med nedsat syn forstørrer teksten via systemindstillinger. Det er vigtigt, at din app respekterer disse præferencer — ellers risikerer du at tvinge brugerne til at bruge zoom, som giver en langt dårligere oplevelse.

<!-- Brug Named Font Sizes, der skalerer med systemets tekststørrelse -->
<Label Text="Denne tekst skalerer med brugerens præference"
       FontSize="{OnPlatform iOS=Body, Android=Body}" />

<!-- Undgå faste pixelstørrelser til tekst -->
<!-- ❌ FontSize="12" -->
<!-- ✅ FontSize="Body" eller FontSize="Medium" -->

Fokusstyring og navigationsrækkefølge

Korrekt fokusrækkefølge sikrer, at skærmlæserbrugere navigerer din app i en logisk sekvens. Det lyder måske som en detalje, men det kan gøre forskellen mellem en app der er brugbar og en der er frustrerende.

SemanticFocus

Du kan programmatisk flytte skærmlæserens fokus til et bestemt element med SetSemanticFocus. Det er især nyttigt efter valideringsfejl eller navigationshandlinger:

// Flyt fokus til en fejlbesked efter validering
private void OnFormValidationFailed(object sender, EventArgs e)
{
    errorLabel.Text = "Venligst udfyld alle påkrævede felter";
    errorLabel.SetSemanticFocus();
}

// Flyt fokus efter navigation eller indholdsændring
private async void OnItemAdded(object sender, EventArgs e)
{
    await DisplayAlert("Succes", "Element tilføjet", "OK");
    itemListView.SetSemanticFocus();
}

SemanticScreenReader.Announce

Brug SemanticScreenReader.Announce til at give skærmlæserbrugere besked om dynamiske ændringer, der sker uden at fokus flyttes. Tænk på det som en slags "breaking news" for skærmlæseren:

// Annoncér statusændringer
private async void OnDataLoaded(object sender, EventArgs e)
{
    SemanticScreenReader.Announce("Data er indlæst. 15 elementer fundet.");
}

// Annoncér fejl
private void OnNetworkError(object sender, EventArgs e)
{
    SemanticScreenReader.Announce("Netværksfejl. Tjek din internetforbindelse og prøv igen.");
}

Praktisk eksempel: En tilgængelig login-side

Okay, lad os samle det hele i et konkret eksempel. Her er en fuldt tilgængelig login-side — det er den slags skærm, som stort set alle apps har, og den er et godt sted at starte med tilgængelighed:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MinApp.LoginPage"
             Title="Log ind">
    <ScrollView>
        <VerticalStackLayout Padding="24" Spacing="16">

            <!-- Overskrift -->
            <Label Text="Log ind på din konto"
                   SemanticProperties.HeadingLevel="Level1"
                   FontSize="24"
                   FontAttributes="Bold" />

            <!-- E-mail felt -->
            <Label Text="E-mail"
                   x:Name="EmailLabel"
                   FontSize="16" />
            <Entry x:Name="EmailEntry"
                   Placeholder="[email protected]"
                   Keyboard="Email"
                   SemanticProperties.Hint="Indtast din registrerede e-mailadresse"
                   ReturnType="Next" />

            <!-- Adgangskode felt -->
            <Label Text="Adgangskode"
                   x:Name="PasswordLabel"
                   FontSize="16" />
            <Entry x:Name="PasswordEntry"
                   Placeholder="Adgangskode"
                   IsPassword="True"
                   SemanticProperties.Hint="Indtast din adgangskode. Minimum 8 tegn."
                   ReturnType="Done" />

            <!-- Fejlbesked (skjult som standard) -->
            <Label x:Name="ErrorLabel"
                   TextColor="#D32F2F"
                   FontSize="14"
                   IsVisible="False"
                   SemanticProperties.HeadingLevel="None" />

            <!-- Log ind knap -->
            <Button Text="Log ind"
                    SemanticProperties.Hint="Dobbelttryk for at logge ind med de indtastede oplysninger"
                    Clicked="OnLoginClicked"
                    HeightRequest="48"
                    FontSize="16" />

            <!-- Glemt adgangskode -->
            <Button Text="Glemt adgangskode?"
                    SemanticProperties.Description="Nulstil adgangskode"
                    SemanticProperties.Hint="Dobbelttryk for at gå til nulstilling af adgangskode"
                    Clicked="OnForgotPasswordClicked"
                    BackgroundColor="Transparent"
                    TextColor="#1565C0"
                    HeightRequest="44" />

        </VerticalStackLayout>
    </ScrollView>
</ContentPage>

Og her er den tilhørende code-behind med tilgængelig fejlhåndtering. Læg særligt mærke til hvordan vi bruger både SetSemanticFocus og SemanticScreenReader.Announce til at sikre, at skærmlæserbrugere får besked om hvad der sker:

public partial class LoginPage : ContentPage
{
    public LoginPage()
    {
        InitializeComponent();
    }

    private async void OnLoginClicked(object sender, EventArgs e)
    {
        // Validering
        if (string.IsNullOrWhiteSpace(EmailEntry.Text) ||
            string.IsNullOrWhiteSpace(PasswordEntry.Text))
        {
            ErrorLabel.Text = "Venligst udfyld både e-mail og adgangskode";
            ErrorLabel.IsVisible = true;

            // Flyt fokus til fejlbeskeden for skærmlæserbrugere
            ErrorLabel.SetSemanticFocus();

            // Annoncér fejlen
            SemanticScreenReader.Announce(ErrorLabel.Text);
            return;
        }

        try
        {
            // Annoncér indlæsning
            SemanticScreenReader.Announce("Logger ind. Vent venligst.");

            // Login-logik her...
            await PerformLoginAsync(EmailEntry.Text, PasswordEntry.Text);

            SemanticScreenReader.Announce("Login vellykket. Navigerer til forsiden.");
        }
        catch (Exception ex)
        {
            ErrorLabel.Text = "Login mislykkedes. Tjek dine oplysninger og prøv igen.";
            ErrorLabel.IsVisible = true;
            ErrorLabel.SetSemanticFocus();
            SemanticScreenReader.Announce(ErrorLabel.Text);
        }
    }

    private async Task PerformLoginAsync(string email, string password)
    {
        // Implementer din login-logik
        await Task.Delay(1000); // Simuleret netværkskald
    }

    private async void OnForgotPasswordClicked(object sender, EventArgs e)
    {
        await Shell.Current.GoToAsync("//forgot-password");
    }
}

Test din apps tilgængelighed

Du kan skrive nok så flot tilgængelighedskode — men hvis du ikke tester det ordentligt, ved du reelt ikke, om det virker. Test bør være en integreret del af din udviklingsproces, ikke noget du gør til sidst (selvom vi alle har været skyldige i det).

Automatiserede værktøjer

  • Android Accessibility Scanner: Analyserer din app-skærm og giver forslag til forbedringer — touch-target størrelse, indholdslabels, kontrast osv.
  • iOS Accessibility Inspector: Integreret i Xcode. Viser tilgængelighedsegenskaber, værdier og handlinger for alle elementer
  • Colour Contrast Analyser: Et standalone-værktøj til at kontrollere kontrastforhold mellem forgrunds- og baggrundsfarver

Manuel testning med skærmlæser

Automatiserede værktøjer er gode, men de fanger langtfra alt. Manuel test med en rigtig skærmlæser er uundværlig:

  1. Aktivér skærmlæseren på enheden (TalkBack/VoiceOver/Narrator)
  2. Navigér gennem hele appen kun med swipe-gestus — prøv at gøre det uden at se på skærmen
  3. Verificér, at alle interaktive elementer er fokusbare
  4. Kontrollér, at navigationsrækkefølgen er logisk og giver mening
  5. Lyt efter, at beskrivelser er meningsfulde i kontekst
  6. Test fejlscenarier — annonceres fejlbeskeder korrekt?

Tilgængelighedstjekliste for .NET MAUI

Brug denne tjekliste som udgangspunkt for dine tilgængelighedsreviews:

  • ☐ Alle billeder har en SemanticProperties.Description eller er skjult fra tilgængelighedstræet
  • ☐ Overskrifter bruger korrekte HeadingLevel-værdier
  • ☐ Touch-targets er minimum 48×48 dp (Android) / 44×44 pt (iOS)
  • ☐ Farvekontrast opfylder WCAG 2.1 AA-krav (4.5:1 for tekst)
  • ☐ Formularer har beskrivende labels og hint-tekst
  • ☐ Dynamiske ændringer annonceres med SemanticScreenReader.Announce
  • ☐ App respekterer systemets tekststørrelsesindstillinger
  • ☐ Ingen information formidles udelukkende via farve

Ofte stillede spørgsmål

Hvad er forskellen på SemanticProperties og AutomationProperties i .NET MAUI?

SemanticProperties er den anbefalede tilgang i .NET MAUI og er designet specifikt til at give skærmlæsere tilgængelighedsoplysninger. AutomationProperties stammer fra Xamarin.Forms og er stadig understøttet af hensyn til bagudkompatibilitet, men Microsoft anbefaler klart, at du bruger SemanticProperties i nye projekter. Den store fordel er, at SemanticProperties respekterer platformens native tilgængelighedsadfærd i stedet for at tvinge en ensartet oplevelse på tværs af platforme.

Gælder European Accessibility Act (EAA) for min app?

Kort svar: sandsynligvis ja, hvis din app er tilgængelig i en app-butik i et europæisk land og falder inden for de omfattede kategorier (e-handel, bank, transport, telekommunikation). EAA gælder uanset, om din virksomhed er baseret i EU eller ej. Den eneste undtagelse er mikroforetagelser med færre end 10 ansatte og en årlig omsætning under €2 millioner — og selv denne undtagelse gælder kun for tjenestekrav, ikke produktkrav.

Hvordan tester jeg tilgængelighed i .NET MAUI uden en fysisk enhed?

Du kan bruge Android-emulatoren med TalkBack aktiveret (via Indstillinger → Tilgængelighed) og iOS-simulatoren med Accessibility Inspector i Xcode. Men ærligt talt anbefaler jeg altid at supplere med test på fysiske enheder — skærmlæseroplevelsen kan variere fra emulator til virkelighed på måder, der overrasker dig. Til automatiseret testning kan du integrere Appium med tilgængelighedsassertions i din CI/CD-pipeline.

Kan jeg bruge .NET MAUI Community Toolkit til tilgængelighed?

Ja! .NET MAUI Community Toolkit tilbyder SemanticOrderView, der giver dig kontrol over den rækkefølge, skærmlæseren navigerer elementer i. Det er rigtig nyttigt, når den visuelle rækkefølge ikke matcher den logiske læserækkefølge (hvilket sker oftere, end man tror). Toolkit indeholder desuden StatusBarBehavior og andre behaviors, der kan forbedre den generelle brugeroplevelse.

Hvad er minimumskravene til farvekontrast ifølge WCAG 2.1 AA?

WCAG 2.1 niveau AA kræver et kontrastforhold på minimum 4.5:1 for normal tekst og 3:1 for stor tekst (18pt og derover eller 14pt fed). UI-komponenter og grafiske objekter kræver minimum 3:1. Du kan bruge værktøjer som Colour Contrast Analyser eller WebAIMs kontrasttjek til at verificere dine farvevalg — det tager kun et par sekunder og kan spare dig for en masse besvær senere.

Om Forfatteren Editorial Team

Our team of expert writers and editors.