@fiscozen/textarea
v3.0.0
Published
Design System Textarea component
Keywords
Readme
@fiscozen/textarea
Multi-line text input component with label, validation states, resize control, and WCAG 2.1 AA accessibility.
For usage documentation, see Storybook Documentation
Development
Setup
pnpm install
pnpm --filter @fiscozen/textarea buildArchitecture
FzTextarea is a form component that wraps a native <textarea> element with label, validation states (error/valid), help/error messages, and full ARIA accessibility. Error messages use FzAlert for consistency with FzInput.
Key design decisions:
- Auto-generated IDs: When no
idprop is provided, a unique ID is generated viautils.tsto ensure label-textarea association always works. Uses timestamp + counter pattern (same as@fiscozen/input). - ARIA ternary pattern: Boolean ARIA attributes use ternary expressions (e.g.
:aria-invalid="error ? 'true' : 'false'") because Vue 3 removes boolean attributes whenfalse, but ARIA spec requires string values. - Error via FzAlert: Error messages use
<FzAlert tone="error" variant="text">withrole="alert", matchingFzInputpattern. Thecircle-xmarkicon is rendered automatically by FzAlert. Dependency:@fiscozen/alert. - Slot-based messages: Error and help content are provided via
errorMessageandhelpTextslots, matchingFzInputpattern. UsesdefineSlotsfor TypeScript typing anduseSlots()for runtime slot detection (same approach asFzInput). - Custom focus ring: Browser default outline is suppressed (
outline-none focus:ring-0 focus:outline-none) and replaced with a border color change on focus (focus:border-blue-600). In error state, focus usesfocus:border-semantic-error-300to maintain error context. This approach differs fromFzInputwhich useshas-[:focus]on a wrapper div; here the border is directly on the<textarea>element. - Design token alignment (Figma): Border colors use
semantic-error-200(default error) /semantic-error-300(error+focus). Placeholder usesplaceholder:text-grey-300. Backgroundbg-core-white, texttext-core-black. Text is alwaystext-base(16px) matching Figma spec (no size variants). Label and help text usefont-normal text-base. Disabled/readonly states applytext-grey-300to label and help text. - Deprecated
sizeprop: Thesizeprop (sm | md | lg) is accepted for backward compatibility but ignored. The textarea always renders withtext-base. A runtimeconsole.warnis emitted when any value is passed. Will be removed in the next major version. inheritAttrs: false+v-bind="$attrs": Extra attributes and native event listeners are forwarded directly to the native<textarea>element, not the root wrapper div. No customdefineEmitsfor DOM events — blur, focus, paste, keydown, etc. all flow through$attrsautomatically.defineExpose({ textareaRef }): Exposes the native<textarea>element ref for programmatic focus or measurement, following the same pattern asFzInput(inputRef,containerRef).- Label ID +
aria-labelledby: Label element gets an explicitid({effectiveId}-label) and the textarea usesaria-labelledbyto reference it, providing stronger screen reader association alongsidefor/idbinding. Same pattern asFzInput. watchfor deprecation: Useswatch(() => props.size, ..., { immediate: true })instead ofonMountedfor deprecation warnings, aligning withFzInput's watcher-based approach. Catches runtime prop changes.- Root identification class:
fz-textareaclass on root div for debugging and external styling, mirroringFzInput'sfz-inputclass. - Combined states: Error+disabled, required+help, disabled+compiled are all supported. When error+disabled, both
aria-invalidandaria-disabledare set to "true", and the error message remains visible. Help text is greyed out when disabled/readonly. - Minimum width/height:
min-w-[96px]prevents horizontal collapse;min-h-[77px]enforces minimum height withrows: 2default (2 rows content + 10px padding top + 10px padding bottom + 1px border top + 1px border bottom). - Auto-height: When
autoHeightistrue, the textarea height is adjusted programmatically on everyv-modelchange viaadjustHeight(). AResizeObserveris used for external size changes. Vertical resize is disabled; horizontal is preserved. Font metrics (lineHeight,paddingY,borderY) are cached once at mount because they are fixed by the design system.maxRowscaps the growth; beyond that a scrollbar appears (overflow-y: auto).autoHeightmust be set at mount — runtime changes are not supported because hooks (onMounted,watch) are registered conditionally at setup time.
Code Organization
src/FzTextarea.vue: Main component (imports FzAlert, FzIcon)src/types.ts: Type definitions (FzTextareaProps)src/utils.ts: ID generation utility (generateTextareaId)src/index.ts: Public exports
Dependencies
@fiscozen/alert(workspace): FzAlert for error messages@fiscozen/icons(peer): FzIcon for valid check icon
Key Concepts
Effective ID
The component computes an effectiveId that falls back to an auto-generated ID when no explicit id prop is provided. This ensures:
- Label
forattribute always matches textareaid - Label
id({effectiveId}-label) is referenced byaria-labelledbyon the textarea aria-describedbyreferences (error/help IDs) are always valid- Error message container ID (
{effectiveId}-error) and help message ID ({effectiveId}-help) are deterministic
Auto Height Algorithm
When autoHeight is enabled:
- Mount:
measureMetrics()readslineHeight,paddingTop+paddingBottom,borderTopWidth+borderBottomWidthfromgetComputedStyle(). These are cached for the component lifetime. - On input (
watch(model)):adjustHeight()is called afternextTick. It resetsheightto"auto"soscrollHeightreflects the natural content height, then computes the constrained height: ifmaxRowsis set andscrollHeight > maxRows * lineHeight + paddingY, it caps the height and setsoverflow-y: auto; otherwiseoverflow-y: hidden. ResizeObserver: Watches for external width changes (e.g. container resize) that could reflow text, callingadjustHeight()to recalculate.- Cleanup: Observer is disconnected in
onBeforeUnmount.
The autoHeightResizeMap object maps resize prop values to their auto-height equivalent, stripping vertical resize while preserving horizontal.
Slot Priority
When both errorMessage and helpText slots are provided, error takes precedence (only shown when error prop is true). The ariaDescribedBy computed uses useSlots() (runtime) to detect which slots are provided, following the same priority: error ID > help ID. This matches FzInput's approach where defineSlots is only for TypeScript typing.
Testing
Running Tests
pnpm --filter @fiscozen/textarea test:unit
pnpm --filter @fiscozen/textarea coverageTest Structure
Tests are organized in nested describe blocks:
- Rendering: Basic component rendering, FzAlert integration
- Props: Each prop has its own
describeblock - Events: Native event forwarding via $attrs, v-model
- Expose:
textareaRefavailability and type - Accessibility: ARIA attributes (including
aria-labelledby), role="alert", decorative icons, keyboard navigation - CSS Classes: Static and dynamic class application
- Edge Cases: Null/undefined/empty values, special characters, combined states (error+disabled, help+disabled, required+help)
- Auto Height: Resize class mapping, warnings (maxRows without autoHeight, vertical resize), height adjustment, overflow-y behavior
- Snapshots: All major states and combinations with explicit IDs for stability
Snapshot Stability
Snapshot tests provide explicit id props to avoid auto-generated IDs (which contain timestamps) from causing flaky comparisons across test runs.
Adding Features
- Update types in
src/types.tswith JSDoc - Implement logic in
src/FzTextarea.vue - Add tests in
src/__tests__/FzTextarea.spec.ts - Update Storybook stories in
apps/storybook/src/stories/form/Textarea.stories.ts - Update MDX documentation in
apps/storybook/src/FzTextarea.mdx
Build
pnpm --filter @fiscozen/textarea buildBreaking Changes
Removed errorMessage and helpMessage props (replaced with slots)
The errorMessage and helpMessage props have been removed. Error and help content are now provided via named slots, aligning with FzInput and FzSelect patterns.
Before:
<FzTextarea label="Notes" :error="hasError" errorMessage="Error text" helpMessage="Help text" />After:
<FzTextarea label="Notes" :error="hasError">
<template #errorMessage>Error text</template>
<template #helpText>Help text</template>
</FzTextarea>Deprecated size prop
The size prop is accepted but ignored. The textarea always uses text-base (16px). A runtime warning is emitted when any value is passed. Will be removed in the next major version.
Removed FzTextareaEvents type and explicit event emits
Native DOM events (blur, focus, paste, etc.) are no longer declared via defineEmits. They are forwarded automatically to the native <textarea> via v-bind="$attrs" (with inheritAttrs: false). The FzTextareaEvents type has been removed from exports.
Before:
<FzTextarea label="Notes" @blur="onBlur" />
<!-- Worked via defineEmits + explicit emit('blur', $event) -->After:
<FzTextarea label="Notes" @blur="onBlur" @keydown="onKeydown" />
<!-- Works via $attrs forwarding. Any native event is supported. -->