Tessera UI

Components

Textarea

A text box for more than one line, with a visible label, an optional hint and an optional error. The label is required: it is what assistive technology reads.

The preview needs JavaScript. The code is in the Code tab.

Textarea-0.tess
<template>
  <div>
    <Textarea rows="3">
      <span slot="label">Message</span>
      <span slot="hint">Plain text, up to 500 characters.</span>
    </Textarea>
    <Textarea error="Write at least a few words.">
      <span slot="label">Feedback</span>
    </Textarea>
  </div>
</template>

Installation

npx @pixelscriptio/tessera add Textarea
pnpm exec tessera add Textarea
yarn tessera add Textarea
bunx @pixelscriptio/tessera add Textarea
  • Works with Tessera 0.9.0 or newer.
  1. Copy and paste the code into your project

    Put it in system/components.

    system/components/Textarea.tess
    <contract>
    {
      "name": "Textarea",
      "role": "generic",
      "description": "A text box for more than one line, with a visible label, an optional hint and an optional error. The label is required: it is what assistive technology reads.",
      "since": "0.8.0",
      "parts": ["root", "label", "input", "hint", "error"],
      "variants": {
        "size": ["md", "sm"] // first value is the default
      },
      "states": ["hover", "focus-visible", "disabled"],
      "slots": {
        "label": "the visible label, required",
        "hint": "help under the box"
      },
      "events": {
        "input": "fired with the new text on every change",
        "change": "fired with the text when the person leaves the box after changing it"
      },
      "preview": {
        "slots": {
          "label": "Message",
          "hint": "Plain text, up to 500 characters."
        }
      }
    }
    </contract>
    
    <script>
    const { value, placeholder, rows, disabled, invalid, error } = props({
      value: '',
      placeholder: '',
      rows: 3,
      disabled: false,
      invalid: false,
      error: '',
    })
    
    // The box owns what is typed, starts from the prop and follows it when it changes.
    const text = state(value.value)
    effect(() => {
      text.value = value.value
    })
    
    // Ids of its own, so the hint and the error can describe the box however many there are.
    const uid = `t-textarea-${Math.random().toString(36).slice(2, 9)}`
    
    function typed(event) {
      emit('input', event.target.value)
    }
    
    function committed(event) {
      emit('change', event.target.value)
    }
    </script>
    
    <template>
      <label class="root">
        <span class="label"><slot name="label" /></span>
        <textarea
          class="input"
          :rows="rows"
          :placeholder="placeholder"
          :disabled="disabled"
          :aria-invalid="invalid || error ? 'true' : null"
          :aria-describedby="uid + '-hint ' + uid + '-error'"
          bind:value="text"
          @input="typed($event)"
          @change="committed($event)"
          ></textarea>
        <span class="hint" :id="uid + '-hint'"><slot name="hint" /></span>
        <span class="error" :id="uid + '-error'" aria-live="polite">{error}</span>
      </label>
    </template>
    
    <style>
    .root {
      display: flex;
      flex-direction: column;
      gap: $space.1;
      color: $color.text.default;
      font-family: var(--t-font-family-sans);
      font-size: $font.size.sm;
    }
    
    .label {
      font-size: $font.size.sm;
      font-weight: $font.weight.bold;
    }
    
    .input {
      padding: $space.2 $space.3;
      border: 1px solid $color.border.strong;
      border-radius: $radius.md;
      background: $color.surface.default;
      color: $color.text.default;
      font: inherit;
      resize: vertical;
      transition-duration: $motion.duration.fast;
    
      &:hover:not(:disabled) {
        border-color: $color.text.default;
      }
    
      &:focus-visible {
        outline: 2px solid $color.focus.ring;
        outline-offset: 2px;
      }
    
      &:disabled {
        opacity: $opacity.disabled;
        cursor: not-allowed;
      }
    
      &[aria-invalid='true'] {
        border-color: $color.action.danger.text;
      }
    }
    
    .root[data-size='sm'] .input {
      padding: $space.1 $space.2;
    }
    
    .hint {
      color: $color.text.muted;
      font-size: $font.size.sm;
    }
    
    .error {
      color: $color.action.danger.text;
      font-size: $font.size.sm;
    }
    
    .hint:empty,
    .error:empty {
      display: none;
    }
    </style>
    
  2. Check your design tokens

    Your design system needs these. tessera add reports the ones that are missing; by hand, run npx @pixelscriptio/tessera check.

    color.action.danger.text color.border.strong color.focus.ring color.surface.default color.text.default color.text.muted font.size.sm font.weight.bold motion.duration.fast opacity.disabled radius.md space.1 space.2 space.3

Usage

Using Textarea

A text box for more than one line: a message, a bio, a description.

  • The label is required. It is what assistive technology reads. Do not use the placeholder as the label.
  • Use the hint slot for help, and the error prop for what is wrong. The error is announced when it appears, is read with the box, and turns the border red. Setting error also marks the box invalid, so you do not need invalid as well.
  • rows is how tall it starts (3 by default). The person can drag it taller.
  • input fires on every change with the new text, and change fires when the person leaves the box.

The preview needs JavaScript. The code is in the Code tab.

Textarea-0.tess
<template>
  <div>
    <Textarea rows="3">
      <span slot="label">Message</span>
      <span slot="hint">Plain text, up to 500 characters.</span>
    </Textarea>
    <Textarea error="Write at least a few words.">
      <span slot="label">Feedback</span>
    </Textarea>
  </div>
</template>

API reference

Variants

VariantValues
sizemd default sm

Slots

SlotContent
labelthe visible label, required
hinthelp under the box

Events

EventWhen
inputfired with the new text on every change
changefired with the text when the person leaves the box after changing it

Anatomy

Rolegeneric
Partsroot label input hint error
Stateshover focus-visible disabled
Since0.8.0

Design tokens it uses

Your design system needs these. tessera add checks that it has them, and can add the ones it lacks.

color.action.danger.text color.border.strong color.focus.ring color.surface.default color.text.default color.text.muted font.size.sm font.weight.bold motion.duration.fast opacity.disabled radius.md space.1 space.2 space.3

Checksum

b9cf09701d21e05dd2ffc3b79112d8d317e2a6efaefd40e12554bfb7cfd5931e