Label
Names a control, and wires the association up for you.
Label renders the <label> for a control and points it at the right element.
Inside a Field it needs no props at all; outside one,
give it a for.
<template>
<div class="flex w-[17rem] flex-col gap-6">
<Field.Root class="group flex flex-col gap-1.5">
<div :class="box">
<div :class="stack">
<span :class="line">
<Input v-model="workspace" :class="control" placeholder=" " />
</span>
<Label :class="label">Workspace</Label>
</div>
</div>
</Field.Root>
<Field.Root
class="group flex flex-col gap-1.5"
:options="{ disabled: true }"
>
<div :class="box">
<div :class="stack">
<span :class="line">
<Input
:class="control"
model-value="eu-central-1"
placeholder=" "
/>
</span>
<Label :class="label">Region</Label>
</div>
</div>
</Field.Root>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Field, Input, Label } from '@maas/mirror/vue'
const workspace = ref('')
const box = [
'relative isolate flex h-12 w-full cursor-text items-center justify-between gap-1.5',
'rounded-component-lg border-surface border-2 px-[0.875rem]',
'transition-all duration-100 ease-linear [&_*]:transition-all [&_*]:duration-100 [&_*]:ease-linear',
'outline-4 outline-transparent focus-within:focus-ring',
'group-[[data-invalid=true]:not([data-focused=true])]:border-danger-subtle',
'group-[[data-invalid=true]:not([data-focused=true])]:bg-danger-subtle',
'group-data-[disabled=true]:border-disabled-subtle group-data-[disabled=true]:cursor-not-allowed',
].join(' ')
const stack = [
'group/stack flex h-full max-h-full w-full flex-col-reverse',
'items-center justify-center gap-0 px-1',
'focus-within:gap-1 has-[input:not(:placeholder-shown)]:gap-1',
].join(' ')
const line = [
'relative flex h-0 w-full items-end',
'group-focus-within/stack:h-[0.9375rem]',
'has-[input:not(:placeholder-shown)]:h-[0.9375rem]',
].join(' ')
const control = [
'type-component-lg leading-[normal]! text-surface',
'block box-content h-[1lh] w-full py-1.25 -my-1.25 appearance-none bg-transparent outline-none',
'placeholder:text-transparent',
'group-[[data-invalid=true]:not([data-focused=true])]:text-danger-on-muted',
'data-[disabled=true]:text-disabled-solid',
].join(' ')
const label = [
'type-component-lg leading-[normal]! text-surface-muted',
'flex h-full w-full items-center [--mirror-label-cursor:text]',
"before:absolute before:-inset-0.5 before:-z-10 before:content-['']",
'group-focus-within/stack:h-auto group-focus-within/stack:text-[0.6875rem] group-focus-within/stack:[--mirror-label-cursor:auto]',
'group-has-[input:not(:placeholder-shown)]/stack:h-auto group-has-[input:not(:placeholder-shown)]/stack:text-[0.6875rem] group-has-[input:not(:placeholder-shown)]/stack:[--mirror-label-cursor:auto]',
'group-[[data-invalid=true]:not([data-focused=true])]:text-danger-muted',
'group-data-[disabled=true]:text-disabled-muted',
].join(' ')
</script>
Usage guidelines
- Give a second label in the same field its own
id, or both take the field’s label ID and register it twice. - A non-native label needs something to point at, so without a
forand without aFieldto inject one it throwsmissing_label_target. - Inside a
Field, a non-native label names the control from the other side: the control addsaria-labelledbyfor it, whether or not it could have been a<label for>target.
Anatomy
Render it beside the control it names.
| Name | Required | Description |
|---|---|---|
| Label | true | Renders a <label>, or a <span> when nativeLabel is false. |
<script setup lang="ts">
import { Field, Input, Label } from '@maas/mirror/vue'
</script>
<template>
<Field.Root>
<Label>Email</Label>
<Input v-model="email" />
</Field.Root>
</template>
Examples
Inside a field
for points at the field’s control, and the label registers its own ID back, so
a control that cannot be a <label for> target names itself through
aria-labelledby instead.
<template>
<Field.Root :options="{ name: 'email' }">
<Label>Email</Label>
<Input v-model="email" />
</Field.Root>
</template>
The label writes the field state set onto its element, so it paints itself from the control’s validity without a bound class.
<template>
<Field.Root :options="{ name: 'email', validate: validateEmail }">
<Label>Email</Label>
<Input v-model="email" />
</Field.Root>
</template>
<style>
.mirror-label[data-invalid='true'] {
color: var(--app-color-danger-fg-muted);
}
.mirror-label[data-required='true']::after {
content: '*';
}
</style>
Outside a field
Given a for, it behaves as a plain <label> with data-scope and nothing
else, since there is no field state for it to report.
<template>
<Label for="search">Search</Label>
<input id="search" type="search" />
</template>
Non-native labels
Some controls cannot be the target of a native <label for>. Set nativeLabel
to false for those and Label renders a <span> with an ID instead.
<template>
<Label id="volume-label" for="volume" :native-label="false">Volume</Label>
<div id="volume" role="slider" tabindex="0" aria-labelledby="volume-label" />
</template>
Inside a Field the wiring is done for you. The label drops its for and the
field’s control picks up aria-labelledby in its place, so the association
holds even over an Input that a native label could have named.
<template>
<Field.Root :options="{ name: 'volume' }">
<Label :native-label="false">Volume</Label>
<Input v-model="volume" />
</Field.Root>
</template>
API reference
Standalone. Ordinary props, no store. Field.Label is this component,
re-exported under the Field namespace.
Label
Renders a <label>, or a <span> when nativeLabel is false.
Props
Slot props
The field state set.
Data attributes
The field state set, plus data-scope="label".
CSS variables
Errors
Accessibility
No roles and no keyboard behaviour of its own. A click forwards focus to the
control, and a non-native label cancels the default on pointerdown and calls
focus() on the target, so a custom control behaves the same way. Inside a
Field, a non-native label is listed in the control’s aria-labelledby, so
the naming holds either way.
data-disabled comes from the field, so a Label given a for outside a field
never reports one and you should style the control instead.