Skip to content

Label

Names a control, and wires the association up for you.

View source View as Markdown

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.

App.vue
<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 for and without a Field to inject one it throws missing_label_target.
  • Inside a Field, a non-native label names the control from the other side: the control adds aria-labelledby for it, whether or not it could have been a <label for> target.

Anatomy

Render it beside the control it names.

NameRequiredDescription
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

PropTypeDefault
id
stringthe field’s labelId, else generated
for
stringinjected controlId
nativeLabel
booleantrue

Slot props

The field state set.

Data attributes

The field state set, plus data-scope="label".

AttributeValue
data-disabled
true
data-readonly
true
data-required
true
data-valid
true
data-invalid
true
data-dirty
true
data-touched
true
data-filled
true
data-focused
true
data-scope
label

CSS variables

VariableDefault
--mirror-label-cursor
default
--mirror-label-disabled-cursor
not-allowed
--mirror-label-user-select
none

Errors

Code
missing_label_target

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.