Skip to content

cv-textarea

Multi-line text input with native textarea semantics and headless state delegation.

Headless: createTextarea

Usage

View source
html
<div class="textarea-demo-shell" data-demo="textarea" data-live-demo-height="980">
  <section class="textarea-demo-hero" aria-labelledby="textarea-demo-title">
    <div class="textarea-demo-copy">
      <span class="textarea-demo-kicker">Multi-line field primitive</span>
      <h3 id="textarea-demo-title">Use textarea when a visible note needs native editing and form state.</h3>
      <p>
        The headless textarea model owns value, focus, disabled/readonly state, geometry, resize, and length
        constraints. UIKit renders the field surface and forwards label, description, and error contracts from
        <code>cv-field</code>.
      </p>
    </div>

    <dl class="textarea-demo-metrics" aria-label="Textarea contract summary">
      <div>
        <dt>States</dt>
        <dd>focused / filled / invalid / readonly / disabled</dd>
      </div>
      <div>
        <dt>Geometry</dt>
        <dd>rows / cols / resize / size tokens</dd>
      </div>
      <div>
        <dt>Events</dt>
        <dd>input / change / focus / blur</dd>
      </div>
    </dl>
  </section>

  <section class="textarea-demo-board" aria-label="Textarea examples in a vault note form">
    <form class="textarea-demo-form" data-textarea-form>
      <div class="textarea-demo-form-head">
        <div>
          <span>Visible vault note</span>
          <strong>border-check / field report</strong>
        </div>
        <cv-badge variant="primary" pill>live contract</cv-badge>
      </div>

      <cv-field required>
        <span slot="label">Operator note</span>
        <cv-textarea
          data-textarea-primary
          name="operator-note"
          rows="7"
          maxlength="260"
          value="Checkpoint asked for the visible profile only.&#10;Relay route stayed in decoy mode.&#10;No hidden vault context was disclosed."
        ></cv-textarea>
        <span slot="description">Edit this note to inspect value, focus, and commit events.</span>
      </cv-field>

      <div class="textarea-demo-actions" aria-label="Textarea sample actions">
        <cv-button data-textarea-action="checkpoint" variant="primary">Load checkpoint note</cv-button>
        <cv-button data-textarea-action="clear" variant="ghost" outline>Clear note</cv-button>
      </div>

      <div class="textarea-demo-field-grid">
        <cv-field invalid>
          <span slot="label">Policy review</span>
          <cv-textarea
            variant="filled"
            resize="none"
            rows="4"
            value="This note references a hidden route by name."
          ></cv-textarea>
          <span slot="error">Visible notes cannot expose hidden-layer identifiers.</span>
        </cv-field>

        <cv-field>
          <span slot="label">Readonly audit excerpt</span>
          <cv-textarea
            readonly
            rows="4"
            value="Signed snapshot: visible profile matched expected decoy route."
          ></cv-textarea>
          <span slot="description">Readonly keeps the native focus target without allowing edits.</span>
        </cv-field>
      </div>
    </form>

    <aside class="textarea-demo-side" aria-label="Textarea event output">
      <div class="textarea-demo-side-head">
        <span class="textarea-demo-kicker">Event stream</span>
        <h4>Interact with the primary note to inspect the public textarea contract.</h4>
      </div>

      <p class="textarea-demo-log" role="status" aria-live="polite" data-textarea-output>
        Waiting for textarea activity. Type, blur, or load a sample note.
      </p>

      <dl class="textarea-demo-live" aria-label="Live textarea state">
        <div>
          <dt>Primary length</dt>
          <dd data-textarea-count>119 / 260</dd>
        </div>
        <div>
          <dt>Last event</dt>
          <dd data-textarea-active>none</dd>
        </div>
        <div>
          <dt>Value state</dt>
          <dd data-textarea-state>filled</dd>
        </div>
      </dl>
    </aside>
  </section>

  <section class="textarea-demo-section" aria-labelledby="textarea-demo-matrix-title">
    <div class="textarea-demo-section-header">
      <span class="textarea-demo-kicker">Variants, size, and field states</span>
      <h4 id="textarea-demo-matrix-title">
        Keep one textarea contract, then tune emphasis with field state, variant, size, resize, and native
        constraints.
      </h4>
    </div>

    <div class="textarea-demo-matrix" aria-label="Textarea state matrix">
      <div>
        <span>Variant</span>
        <cv-textarea rows="3" placeholder="Outlined default"></cv-textarea>
        <cv-textarea variant="filled" rows="3" value="Filled surface"></cv-textarea>
      </div>

      <div>
        <span>Size</span>
        <cv-textarea size="small" rows="2" resize="none" value="Small note"></cv-textarea>
        <cv-textarea rows="2" resize="none" value="Medium note"></cv-textarea>
        <cv-textarea size="large" rows="2" resize="none" value="Large note"></cv-textarea>
      </div>

      <div>
        <span>Resize</span>
        <cv-textarea rows="3" value="Vertical resize enabled"></cv-textarea>
        <cv-textarea resize="none" rows="3" value="Resize disabled for fixed layouts"></cv-textarea>
      </div>

      <div>
        <span>Validation</span>
        <cv-field required invalid>
          <span slot="label">Disclosure note</span>
          <cv-textarea minlength="12" rows="3" value="Too short"></cv-textarea>
          <span slot="error">Provide enough context for the visible route.</span>
        </cv-field>
      </div>

      <div>
        <span>Read state</span>
        <cv-textarea readonly rows="3" value="Readonly but focusable"></cv-textarea>
        <cv-field disabled>
          <span slot="label">Disabled by field</span>
          <cv-textarea rows="3" value="Locked by policy"></cv-textarea>
        </cv-field>
      </div>

      <div>
        <span>Native constraints</span>
        <cv-textarea
          required
          minlength="8"
          maxlength="80"
          rows="3"
          placeholder="Required with min/max"
        ></cv-textarea>
        <cv-textarea cols="34" rows="3" value="Cols remains a native attribute for forms."></cv-textarea>
      </div>
    </div>
  </section>
</div>

<script type="module">
  document
    .querySelectorAll('.textarea-demo-shell[data-demo="textarea"]:not([data-ready])')
    .forEach((shell) => {
      shell.dataset.ready = 'true'

      const output = shell.querySelector('[data-textarea-output]')
      const count = shell.querySelector('[data-textarea-count]')
      const active = shell.querySelector('[data-textarea-active]')
      const state = shell.querySelector('[data-textarea-state]')
      const primaryTextarea = shell.querySelector('[data-textarea-primary]')
      const eventLabels = {
        'cv-input': 'input',
        'cv-change': 'change',
        'cv-focus': 'focus',
        'cv-blur': 'blur',
      }

      const samples = {
        checkpoint: `Checkpoint asked for the visible profile only.
Relay route stayed in decoy mode.
No hidden vault context was disclosed.`,
        clear: '',
      }

      const setOutput = (message) => {
        if (output) output.textContent = message
      }

      const getFieldName = (textarea) => {
        const label = textarea.closest('cv-field')?.querySelector('[slot="label"]')?.textContent?.trim()
        return label || textarea.getAttribute('name') || 'textarea'
      }

      const updatePrimaryState = () => {
        if (!primaryTextarea) return

        const value = primaryTextarea.value || ''
        if (count) count.textContent = `${value.length} / 260`
        if (state) state.textContent = value.trim() ? 'filled' : 'empty'
      }

      const report = (event) => {
        const textarea = event.target instanceof HTMLElement ? event.target : null
        if (!textarea?.matches('cv-textarea')) return

        const name = getFieldName(textarea)
        const label = eventLabels[event.type] ?? event.type
        const value = 'value' in event.detail ? String(event.detail.value) : textarea.value || ''

        if (textarea === primaryTextarea) updatePrimaryState()
        if (active) active.textContent = label
        setOutput(`${label}: ${name} -> ${value.length} characters`)
      }

      shell.querySelectorAll('[data-textarea-action]').forEach((button) => {
        button.addEventListener('click', () => {
          const action = button.getAttribute('data-textarea-action') || ''
          if (!primaryTextarea || !(action in samples)) return

          primaryTextarea.value = samples[action]
          primaryTextarea.focus()
          updatePrimaryState()
          if (active) active.textContent = action
          setOutput(`${action}: Operator note -> ${primaryTextarea.value.length} characters`)
        })
      })

      Object.keys(eventLabels).forEach((eventName) => {
        shell.addEventListener(eventName, report)
      })

      updatePrimaryState()
    })
</script>

Anatomy

<cv-textarea> (host)
└── <div part="base">
    └── <textarea part="textarea"></textarea>

Attributes

AttributeTypeDefaultReflectsDescription
valueString""noCurrent textarea value
placeholderString""noPlaceholder text
disabledBooleanfalseyesPrevents interaction and removes from tab sequence
readonlyBooleanfalseyesPrevents editing while keeping focusability
requiredBooleanfalseyesMarks the field as required
rowsNumber4noVisible row count
colsNumber20noVisible column count
minlengthNumbernoMinimum accepted value length
maxlengthNumbernoMaximum accepted value length
resizeString"vertical"yesResize behavior: vertical | none
sizeString"medium"yesComponent size: small | medium | large
variantString"outlined"yesVisual variant: outlined | filled
nameString""noNative textarea form field name
aria-labelledbyString""noID reference passed to the native textarea
aria-describedbyString""noDescription/error ID reference passed to the native textarea

Variants

VariantDescription
outlinedDefault style with visible border and transparent background
filledSubtle filled surface with transparent border

Sizes

Size--cv-textarea-min-height--cv-textarea-padding-inline--cv-textarea-font-size
small72pxvar(--cv-space-2, 8px)var(--cv-font-size-sm, 13px)
medium96pxvar(--cv-space-3, 12px)var(--cv-font-size-base, 14px)
large120pxvar(--cv-space-4, 16px)var(--cv-font-size-md, 16px)

Slots

cv-textarea has no content slots. Wrap it in cv-field for labels, descriptions, and errors.

The native <textarea> is not slottable. There is no default slot.

CSS Parts

PartElementDescription
base<div>Wrapper around the native textarea
textarea<textarea>Native multi-line text control

CSS Custom Properties

PropertyDefaultDescription
--cv-textarea-min-height96pxMinimum block size of textarea control
--cv-textarea-padding-inlinevar(--cv-space-3, 12px)Horizontal textarea padding
--cv-textarea-padding-blockvar(--cv-space-2, 8px)Vertical textarea padding
--cv-textarea-font-sizevar(--cv-font-size-base, 14px)Textarea font size
--cv-textarea-border-radiusvar(--cv-radius-sm, 6px)Border radius of wrapper and textarea
--cv-textarea-border-colorvar(--cv-color-border, #2a3245)Border color in default state
--cv-textarea-backgroundtransparentBackground for outlined variant
--cv-textarea-colorvar(--cv-color-text, #e8ecf6)Foreground text color
--cv-textarea-placeholder-colorvar(--cv-color-text-muted, #6b7a99)Placeholder color
--cv-textarea-focus-ring0 0 0 2px var(--cv-color-primary, #65d7ff)Focus ring for focused state
--cv-textarea-transition-durationvar(--cv-duration-fast, 120ms)Transition duration for container state changes

Visual States

Host selectorDescription
:host([disabled])Reduced opacity (0.55), no pointer interaction
:host([readonly])Editable appearance with default cursor semantics
:host([required])Required semantic state (no default visual marker)
:host([focused])Focus ring shown on wrapper
:host([filled])Non-empty state for styling hooks
:host([resize="vertical"])Native textarea vertical resize enabled
:host([resize="none"])Native textarea resize disabled
:host([size="small"])Small size token overrides
:host([size="large"])Large size token overrides
:host([variant="outlined"])Outlined variant
:host([variant="filled"])Filled variant

Reactive State Mapping

cv-textarea is a visual adapter over headless createTextarea.

UIKit properties to headless actions

UIKit PropertyDirectionHeadless Binding
valueattr/prop -> actionactions.setValue(value)
disabledattr -> actionactions.setDisabled(value)
readonlyattr -> actionactions.setReadonly(value)
requiredattr -> actionactions.setRequired(value)
placeholderattr -> actionactions.setPlaceholder(value)
rowsattr -> actionactions.setRows(value)
colsattr -> actionactions.setCols(value)
minlengthattr -> actionactions.setMinLength(value)
maxlengthattr -> actionactions.setMaxLength(value)
resizeattr -> actionactions.setResize(value)

Headless state to DOM reflection

Headless StateDirectionDOM Reflection
state.disabled()state -> attr[disabled] host attribute
state.readonly()state -> attr[readonly] host attribute
state.required()state -> attr[required] host attribute
state.focused()state -> attr[focused] host attribute
state.filled()state -> attr[filled] host attribute
state.resize()state -> attr[resize] host attribute

Contract props spreading

  • contracts.getTextareaProps() is spread onto [part="textarea"] to apply id, aria-disabled, aria-readonly, aria-required, placeholder, disabled, readonly, required, tabindex, rows, cols, minlength, and maxlength.
  • Host aria-labelledby and aria-describedby are passed through to the native <textarea>, typically from cv-field.

Event wiring

  • Native textarea input -> actions.handleInput(e.target.value) -> dispatch cv-input
  • Native textarea focus -> actions.setFocused(true) -> dispatch cv-focus
  • Native textarea blur -> actions.setFocused(false) -> dispatch cv-blur; if value changed since focus, dispatch cv-change

UIKit does not own ARIA computation, disabled/readonly guards, or filled-state derivation.

Events

EventDetailDescription
cv-input{ value: string }Fires when user input mutates value
cv-change{ value: string }Fires on blur when value changed since focus
cv-focus{ }Fires when textarea receives focus
cv-blur{ }Fires when textarea loses focus

ChromVoid UIKit documentation