Summary#
Big bold text looks like a heading and is not one. A reader who cannot see the page asks it for its headings and
jumps between them, the way a sighted reader scans the large type — so a title drawn as
Text("Orders", fontSize: FontSize.Title) is a heading to the eye and to nothing else.
The kit's three heading controls say it for you:
| control | heading level | size |
|---|---|---|
PageTitle("Orders") | 1 — one per page | FontSize.Title |
CardTitle("Billing") — and every Card("…") and Dialog("…") title | 2 | FontSize.Subhead |
Heading("Opening hours", level: n) | n, 1 to 6 (default 2) | follows the level |
using Osysharp.Ui;
[Route("/")]
[AllowAnonymous]
[Render(CSR)]
[Title("Café Linden")]
component Home() {
render {
Stack(gap: 4, p: 6) {
PageTitle("Café Linden"); // heading, level 1
Card("Opening hours") { // heading, level 2
Heading("Weekdays", level: 3);
Text("7:00 – 18:00");
Heading("Weekends", level: 3);
Text("9:00 – 16:00");
}
}
}
}Signature#
Heading(string text, int level = 2) // 1–6; outside that range it is drawn and announced as the nearest one
PageTitle(string text) // level 1
CardTitle(string text) // level 2
Text(text, role: UiRole.Heading, level: 3) // the same fact on any element of your ownDescription#
Which level should a heading be?#
The level is the heading's place in the page's outline, not its size: level 1 is what the page IS (one per page —
PageTitle), level 2 a part of it (a card, a section), level 3 a part of that. A reader hears "heading, level 3,
Weekends" and knows it belongs to the level-2 heading before it. Skipping a level is legal and reads as a gap, so
prefer the next level down.
The size follows the level so that saying which heading it is is enough:
| level | size | weight |
|---|---|---|
| 1 | FontSize.Title | the theme's FontWeight.Headings |
| 2 | FontSize.Heading | the same |
| 3 | FontSize.Subhead | the same |
| 4–6 | FontSize.Body | the same |
All of them wear the theme's display face (Font.Display), like PageTitle and CardTitle, so a theme that gives
headings a face of their own reaches every one.
A heading of your own#
Any element can be one: role: UiRole.Heading with level: lowers to role="heading" aria-level="…", which every
screen reader treats exactly as an <h1>–<h6>. Always pass the level — a heading without one is announced as
level 2 whatever it is.
Text("This week", role: UiRole.Heading, level: 2, fontSize: FontSize.Title, color: Colors.Primary);How do I test it?#
A heading is read as text — Assert.Visible("Opening hours"). That it is a heading, and at which level, is a fact for
a real browser: the kit's own UX gate checks PageTitle is the page's level-1 heading and Heading(…, level: 3) a
level-3 one (osy-client/tests/e2e/visual/suites/ux-fixture-a11ykit.mjs).
Examples#
using Osysharp.Ui;
class Dish { public string Name; public string Course; }
[Route("/menu")]
[AllowAnonymous]
[Render(CSR)]
[Title("Menu")]
component MenuPage() {
Dish[] dishes = [
new Dish { Name = "Soup of the day", Course = "Starters" },
new Dish { Name = "Roast squash", Course = "Mains" },
];
render {
Stack(gap: 3, p: 6) {
PageTitle("Menu");
foreach (var course in dishes.Select(d => d.Course).Distinct()) {
Heading(course);
foreach (var d in dishes.Where(x => x.Course == course)) { Text(d.Name); }
}
}
}
}See also#
- accessibility — the whole semantic vocabulary: roles, states, names
- Osysharp.Ui (the UI kit) — every control in the kit
- style props — the type scale the levels are drawn in