# [Label], [Icon], [Tone] — what a human reads

> An enum member stores a compact value but shows a human-readable label. [Label("…")] gives a member its label (otherwise the member's own name is used), and a doc comment gives it a longer description. A screen that shows an enum-typed value — a grid cell, a text line — reads the label, never the stored value. [Icon(…)] and [Tone(…)] say which icon and which tone stand for a member, so every screen shows it the same way without repeating the decision.

<!-- id: enum-labels · area: enum · stability: stable · html: https://osysharp.com/reference/enum/labels/ -->

## Summary        {#summary}
An enum member has two faces. It has a **stored value** — the compact thing that lives in the column and that your
code compares against — and it has a **label**, the words a person reads on screen. `[Label("…")]` sets the label.
Without one, the label is simply the member's name.

You never have to convert between them. Show an enum-typed value anywhere in a screen and the label is what appears.

## Signature      {#signature}
```osy syntax
enum <Name> {
  [Label("<label>")] <Member>,     // an explicit label

  /// <description>
  <Member>,                          // a doc comment becomes the member's description
}
```

## Description    {#description}

### How do I change the words a member shows?   {#label}
A member name is an identifier, so it cannot contain spaces or punctuation. `[Label]` supplies the words:

```osy title="labels a person can read" test app=enum-labels
enum OrganizationType {
  /// A single person's own space.
  Personal,

  [Label("Team or company")] Team,
}

entity Organization {
  [Required, MaxLength(100)] string Name;
  OrganizationType Type = OrganizationType.Team;
}
```

`Team` reads as **Team or company**. `Personal` declares no `[Label]`, so it reads as **Personal** — the member
name is a perfectly good label when it already says what it means, and you should not add `[Label("Personal")]`
just to be explicit.

A doc comment on a member becomes its **description** — a longer sentence a control can show beside the label, such
as the help text under an option in a dropdown. It is optional; a member with no doc comment simply has none.

### How do I show an enum value on a screen?   {#showing}
Anywhere a screen displays an enum-typed value, it displays the label:

```osy title="a list that reads as words" test app=enum-labels
[Page("/orgs")]
[Render(CSR)]
component OrganizationsPage() {
  var orgs = Organization.ToList();

  render {
    Stack(gap: 2) {
      foreach (var o in orgs) {
        Row(gap: 3) {
          Text(o.Name);
          Text(o.Type);        // "Team or company" — the label, not the stored value
        }
      }
    }
  }
}
```

A grid column reads the same way: give it the member's key (`Type`) and the cell shows the label. You never write a
lookup, a mapping, or a `switch` to turn a value into words.

Because the label is resolved only for display, the value itself is untouched — `o.Type == OrganizationType.Team`
still compares against the stored value, exactly as it always did.

### When you need the label as a STRING — `.Label`   {#label-property}
Displaying a value shows its label without being asked. But some props take a **string**, not a value to render — a
`Button`'s label, a `title`, an aria name — and there the label has to be read. `.Label` is that read.

**The difference is the SLOT, not the value** — one table so it never has to be worked out:

| you write | you get | why |
|---|---|---|
| `Text(dish.Cuisine)` | `Street food` — the label | a text slot RENDERS an enum value |
| `Badge(dish.Cuisine.Label, …)` | `Street food` | a `string` PARAMETER takes a string, so read the label |
| `Text("in " + dish.Cuisine)` | `in StreetFood` — the **member name** | `+` is a string op, and an enum's string form is its name (C#-exact) |
| `Text("in " + dish.Cuisine.Label)` | `in Street food` | …so say `.Label` when you concatenate |
| `Row { Text("in "); Text(dish.Cuisine); }` | `in Street food` | …or give the enum its own text slot |

⚑ The third row is the one that surprises, and it is not a compile error — it renders, just with the wrong words.
Two generated apps in a row reasoned their way to the last row from first principles rather than reading it here.

```osy title="a filter button per member, worded properly" test app=enum-label-property
enum Cuisine { [Label("Street food")] StreetFood, Thai, Nordic }

[Page("/cuisines")]
[AllowAnonymous]
component CuisineFilter() {
  Cuisine picked = Cuisine.Thai;
  action Pick(Cuisine c) { picked = c; }

  render {
    Row(gap: 2) {
      foreach (var c in Cuisine.Members) {
        Button(c.Label, onPress: () => Pick(c));   // "Street food", not "StreetFood"
      }
    }
  }
}
```

`Cuisine.Members` is every member of the enum, so a picker is a `foreach` and stays right when a member is added.
`.Description`, `.Name`, `.Icon` and `.Tone` read the rest of a member's presentation the same way — `.Name` is the
member's identifier (`"StreetFood"`), which is what `.ToString()` gives you and almost never what a person should
read.

⚠ They are **properties, not methods**: `r.Label`, never `r.Label()`.

### The stored string must differ from the name — `[Value]`   {#value}
Under `[Type(string)]` an enum member is stored by its **name** by default. When the stored string must differ from the
name — to match an external system, or to keep a stable code while the member is renamed — the **`[Value]`** attribute
sets it explicitly: `[Value("team")] Team` stores `"team"` while your code still writes `OrganizationType.Team`. Most
enums never need it; reach for it only when the storage string is a contract with something outside your app.

### And you read it back with `.Value`   {#read-the-value}
`[Value("…")]` is not write-only. **`.Value` on an enum-typed value is the stored key** — the string you declared,
or the member's own name where you declared none:

```osy title="the declared key, read back" test app=enum-value-readback
[Type(string)] enum TimeSlot {
  [Value("08:00")] Early,
  [Value("10:00")] Mid,
  Late,                          // no [Value] — stores its own name
}

string SlotStart(TimeSlot slot) { return slot.Value; }   // "08:00" / "10:00" / "Late"
```

It is the **fourth word** beside `.Label`, `.Description` and `.Name`, and it is the one to reach for when something
OUTSIDE your app has to receive the value you chose — a URL, a header, a row you are exporting. `.Label` is for a
person, `.Name` is the identifier you wrote, and `.Value` is what the column and the wire hold.

⚠ **Without `[Type(string)]` an enum stores its ordinal, and `.Value` answers that** — the `int`. One spelling,
whichever storage the enum declared.

⚠ **On an OPTIONAL enum, `.Value` still means unwrap**, exactly as in C#. So the key of a `TimeSlot?` is
`slot.Value.Value`: unwrap first, then read the key.

⭐ **Reach for this instead of writing the mapping out.** A hand-written `slot == TimeSlot.Early ? "08:00" : …`
restates the keys the enum already declares, and the two drift the first time somebody edits one of them — which is
the same reason [`<Enum>.Members`](https://osysharp.com/reference/enum/declaration/) exists.

### Which icon and tone stand for a member? — `[Icon]` and `[Tone]`   {#presentation}
A label is not the only thing a member has. "Cancelled" is usually also a **cross**, and usually also a **danger** —
and those are facts about the member, not about the screen that happens to be showing it. Say them once:

```osy title="a member carries its own icon and tone" test app=enum-labels
enum Tone { Neutral, Success, Warning, Danger, Accent }

enum OrderState {
  [Label("Active"),    Icon(check), Tone(Tone.Success)] Active,
  [Label("On hold"),   Icon(pause), Tone(Tone.Warning)] OnHold,
  [Label("Cancelled"), Icon(close), Tone(Tone.Danger)]  Cancelled,
}
```

Without this, every dropdown, cell, badge and header re-decides with its own `if (state == Cancelled)` chain — which
is how the same enum ends up red on one screen and grey on the next.

**`[Icon]` names a declared icon; `[Tone]` names a declared tone.** `Icon(check)` must name an icon your app can
draw — one it declares (an `icons/check.svg`) or one of the built-ins — written unquoted.

⚠ **Here the name is BARE, and at a call site it is qualified.** The attribute takes `Icon(check)`; rendering the
same glyph directly takes `Icon(Icons.Check)`. The two spellings are not interchangeable, and writing the qualified
form in the attribute is a compile error. `Tone(Tone.Danger)` is a **qualified reference to a member of a tone enum**
— the same shape as `[Classification(DataClass.PII)]` — so the referenced member is checked. A typo in either is a
compile error with a suggestion, not a blank space at runtime.

**`[Tone]` names a tone and never a colour.** `[Tone(Tone.Success)]` says the member *is* a success and lets the
design system decide what that looks like — so re-theming the app carries every enum with it, and a dark mode does not
need the enum edited. A hex here would put presentation in your domain model permanently.

Neither has a fallback. A member with no `[Icon]` has no icon, and a screen is free to show nothing — unlike the
label, where something must always be shown.

### Changing a label is safe; changing a name is not   {#renaming}
The label is presentation, so you can reword it freely — no stored data refers to it. The member **name**, by
contrast, is what your code names (`OrganizationType.Team`), and under `[Type(string)]` it is also what the column
stores (unless a `[Value]` pins it). Reword the label when the words are wrong; rename the member only when the concept
is.

## See also       {#see-also}
- [icons](https://osysharp.com/reference/ui/icons/) — declaring the icons `[Icon(…)]` may name
- [enum](https://osysharp.com/reference/enum/declaration/) — declaring an enum and choosing how it is stored
- [entity members](https://osysharp.com/reference/entity/properties/) — using an enum as a member type
- [control — foreign UI controls (charts, grids, maps)](https://osysharp.com/reference/ui/controls/) — controls that show and edit an entity's values
