Phi
InputGroup
@dicehub/phiv1.0.0-beta.1

InputGroup

Compose inputs with addons, icons, buttons, and text for rich form fields.

<script setup>
import { InputGroup } from "@dicehub/phi/components/input-group";
import { PhCheckCircle } from "@phosphor-icons/vue";
import { onBeforeUnmount, ref } from "vue";

const status = ref("success");
const subdomain = ref("phi");
let timer;

const handleChange = (value) => {
  subdomain.value = value;

  if (timer) window.clearTimeout(timer);

  if (value.length > 0) {
    status.value = "loading";
    timer = window.setTimeout(() => {
      status.value = "success";
    }, 1500);
  } else {
    status.value = "idle";
  }
};

onBeforeUnmount(() => {
  if (timer) window.clearTimeout(timer);
});
</script>

<template>
  <InputGroup>
    <InputGroup.Input
      :model-value="subdomain"
      maxlength="20"
      aria-label="Project subdomain"
      @update:model-value="handleChange"
    />
    <InputGroup.Suffix>.example.com</InputGroup.Suffix>
    <InputGroup.Addon v-if="status !== 'idle'" align="end">
      <span v-if="status === 'loading'" class="phi-input-group-spinner" aria-label="Loading" role="status" />
      <PhCheckCircle v-else weight="duotone" />
    </InputGroup.Addon>
  </InputGroup>
</template>

Installation

Barrel

import { InputGroup } from "@dicehub/phi";

Granular

import { InputGroup } from "@dicehub/phi/components/input-group";

Usage

Pass the label prop to InputGroup to enable the built-in Field wrapper with label, description, and error support.

<script setup>
import { InputGroup } from "@dicehub/phi/components/input-group";
import { PhMagnifyingGlass } from "@phosphor-icons/vue";
</script>

<template>
  <InputGroup label="Search" description="Find pages, components, and more">
    <InputGroup.Addon>
      <PhMagnifyingGlass />
    </InputGroup.Addon>
    <InputGroup.Input placeholder="Search..." />
  </InputGroup>
</template>

Bare InputGroup (Custom Layouts)

For custom form layouts, use InputGroup without label. Must providearia-label or aria-labelledby on InputGroup.Input for accessibility.

<script setup>
import { InputGroup } from "@dicehub/phi/components/input-group";
import { PhMagnifyingGlass } from "@phosphor-icons/vue";
</script>

<template>
  <InputGroup>
    <InputGroup.Addon>
      <PhMagnifyingGlass />
    </InputGroup.Addon>
    <InputGroup.Input placeholder="Search..." aria-label="Search" />
  </InputGroup>
</template>

Examples

Icon

Use Addon to place an icon at the start of the input as a visual identifier.

<script setup>
import { InputGroup } from "@dicehub/phi/components/input-group";
import { PhLink } from "@phosphor-icons/vue";
</script>

<template>
  <InputGroup>
    <InputGroup.Addon>
      <PhLink />
    </InputGroup.Addon>
    <InputGroup.Input placeholder="Paste a link..." aria-label="Link" />
  </InputGroup>
</template>

Text

Use Addon to place text prefixes or suffixes alongside the input.

<script setup>
import { InputGroup } from "@dicehub/phi/components/input-group";
</script>

<template>
  <div class="stack">
    <InputGroup>
      <InputGroup.Addon>@</InputGroup.Addon>
      <InputGroup.Input placeholder="username" aria-label="Username" />
    </InputGroup>

    <InputGroup>
      <InputGroup.Input placeholder="email" aria-label="Email" />
      <InputGroup.Addon align="end">@example.com</InputGroup.Addon>
    </InputGroup>

    <InputGroup>
      <InputGroup.Addon>/api/</InputGroup.Addon>
      <InputGroup.Input placeholder="endpoint" aria-label="API path" />
      <InputGroup.Addon align="end">.json</InputGroup.Addon>
    </InputGroup>
  </div>
</template>

Button

Place InputGroup.Button inside an Addon for actions that operate directly on the input value. Buttons stay flat inside the shared group surface.

<script setup>
import { InputGroup } from "@dicehub/phi/components/input-group";
import { PhEye, PhEyeSlash, PhMagnifyingGlass, PhX } from "@phosphor-icons/vue";
import { ref } from "vue";

const show = ref(false);
const searchValue = ref("search");
</script>

<template>
  <div class="stack">
    <InputGroup>
      <InputGroup.Input :type="show ? 'text' : 'password'" default-value="password" aria-label="Password" />
      <InputGroup.Addon align="end">
        <InputGroup.Button
          shape="square"
          :icon="show ? PhEyeSlash : PhEye"
          :aria-label="show ? 'Hide password' : 'Show password'"
          @click="show = !show"
        />
      </InputGroup.Addon>
    </InputGroup>

    <InputGroup>
      <InputGroup.Addon><PhMagnifyingGlass /></InputGroup.Addon>
      <InputGroup.Input v-model="searchValue" placeholder="Search" aria-label="Search" />
      <InputGroup.Addon v-if="searchValue" align="end">
        <InputGroup.Button shape="square" :icon="PhX" aria-label="Clear search" @click="searchValue = ''" />
      </InputGroup.Addon>
      <InputGroup.Button variant="secondary">Search</InputGroup.Button>
    </InputGroup>
  </div>
</template>

Button with Tooltip

Pass a tooltip prop to InputGroup.Button to show a tooltip on hover.

<script setup>
import { InputGroup } from "@dicehub/phi/components/input-group";
import { PhMagnifyingGlass, PhQuestion } from "@phosphor-icons/vue";
</script>

<template>
  <InputGroup>
    <InputGroup.Addon>
      <PhMagnifyingGlass />
    </InputGroup.Addon>
    <InputGroup.Input placeholder="Search with query language..." aria-label="Search" />
    <InputGroup.Addon align="end">
      <InputGroup.Button
        shape="square"
        :icon="PhQuestion"
        aria-label="Query language help"
        tooltip="Query language help"
      />
    </InputGroup.Addon>
  </InputGroup>
</template>

Kbd

Place a keyboard shortcut hint inside an end Addon.

<script setup>
import { InputGroup } from "@dicehub/phi/components/input-group";
import { PhMagnifyingGlass } from "@phosphor-icons/vue";
</script>

<template>
  <InputGroup>
    <InputGroup.Addon>
      <PhMagnifyingGlass />
    </InputGroup.Addon>
    <InputGroup.Input placeholder="Search..." aria-label="Search" />
    <InputGroup.Addon align="end">
      <kbd class="phi-input-group-kbd">⌘K</kbd>
    </InputGroup.Addon>
  </InputGroup>
</template>

Loading

Place a loader inside an end Addon as a status indicator while validating the input value.

<script setup>
import { InputGroup } from "@dicehub/phi/components/input-group";
</script>

<template>
  <InputGroup>
    <InputGroup.Input default-value="phi" aria-label="Project slug" />
    <InputGroup.Addon align="end">
      <span class="phi-input-group-spinner" aria-label="Loading" role="status" />
    </InputGroup.Addon>
  </InputGroup>
</template>

Inline Suffix

Suffix renders text that flows seamlessly next to the typed value.

<script setup>
import { InputGroup } from "@dicehub/phi/components/input-group";
import { PhCheckCircle, PhXCircle } from "@phosphor-icons/vue";
</script>

<template>
  <div class="stack">
    <InputGroup label="Subdomain">
      <InputGroup.Input default-value="phi" maxlength="20" />
      <InputGroup.Suffix>.example.com</InputGroup.Suffix>
      <InputGroup.Addon align="end">
        <PhCheckCircle weight="duotone" />
      </InputGroup.Addon>
    </InputGroup>

    <InputGroup label="Subdomain" error="This subdomain is unavailable">
      <InputGroup.Input default-value="taken" maxlength="20" />
      <InputGroup.Suffix>.example.com</InputGroup.Suffix>
      <InputGroup.Addon align="end">
        <PhXCircle weight="duotone" />
      </InputGroup.Addon>
    </InputGroup>
  </div>
</template>

Sizes

Four sizes: xs, sm, base (default), and lg. The size applies to the entire group. Large groups use a 2px outer inset for trailing addon buttons.

<script setup>
import { InputGroup } from "@dicehub/phi/components/input-group";
import { PhMagnifyingGlass, PhQuestion } from "@phosphor-icons/vue";
</script>

<template>
  <div class="stack">
    <InputGroup size="xs" label="Extra Small">
      <InputGroup.Addon><PhMagnifyingGlass /></InputGroup.Addon>
      <InputGroup.Input placeholder="Extra small input" />
      <InputGroup.Addon align="end">
        <InputGroup.Button shape="square" :icon="PhQuestion" aria-label="Help" />
      </InputGroup.Addon>
    </InputGroup>

    <InputGroup size="sm" label="Small">
      <InputGroup.Addon><PhMagnifyingGlass /></InputGroup.Addon>
      <InputGroup.Input placeholder="Small input" />
      <InputGroup.Addon align="end">
        <InputGroup.Button shape="square" :icon="PhQuestion" aria-label="Help" />
      </InputGroup.Addon>
    </InputGroup>

    <InputGroup label="Base (default)">
      <InputGroup.Addon><PhMagnifyingGlass /></InputGroup.Addon>
      <InputGroup.Input placeholder="Base input" />
      <InputGroup.Addon align="end">
        <InputGroup.Button shape="square" :icon="PhQuestion" aria-label="Help" />
      </InputGroup.Addon>
    </InputGroup>

    <InputGroup size="lg" label="Large">
      <InputGroup.Addon><PhMagnifyingGlass /></InputGroup.Addon>
      <InputGroup.Input placeholder="Large input" />
      <InputGroup.Addon align="end">
        <InputGroup.Button shape="square" :icon="PhQuestion" aria-label="Help" />
      </InputGroup.Addon>
    </InputGroup>
  </div>
</template>

States

Use label, error, disabled, required, and description props directly on InputGroup.

<script setup>
import { InputGroup } from "@dicehub/phi/components/input-group";
import { PhEye, PhEyeSlash, PhMagnifyingGlass } from "@phosphor-icons/vue";
import { ref } from "vue";

const show = ref(false);
</script>

<template>
  <div class="stack">
    <InputGroup label="Error State" error="Please enter a valid email address">
      <InputGroup.Input type="email" default-value="invalid-email" />
      <InputGroup.Addon align="end">@example.com</InputGroup.Addon>
    </InputGroup>

    <InputGroup label="Disabled" disabled>
      <InputGroup.Addon><PhMagnifyingGlass /></InputGroup.Addon>
      <InputGroup.Input placeholder="Search..." />
    </InputGroup>

    <InputGroup label="Optional Field" :required="false">
      <InputGroup.Addon>$</InputGroup.Addon>
      <InputGroup.Input placeholder="0.00" />
    </InputGroup>

    <InputGroup label="With Description" description="Must be at least 8 characters" label-tooltip="Your password is stored securely">
      <InputGroup.Input :type="show ? 'text' : 'password'" placeholder="Password" />
      <InputGroup.Addon align="end">
        <InputGroup.Button
          shape="square"
          :icon="show ? PhEyeSlash : PhEye"
          :aria-label="show ? 'Hide password' : 'Show password'"
          @click="show = !show"
        />
      </InputGroup.Addon>
    </InputGroup>
  </div>
</template>

API Reference

InputGroup

The root container that provides context to all child parts.

PropTypeDefaultDescription
labelstring-Visible field label. Enables the field wrapper.
descriptionstring-Helper text shown below the group when there is no error.
errorstring | { message: string; match?: InputErrorMatch }-Validation error message. Also applies error styling.
invalidbooleanfalseMarks the group invalid without requiring an error message.
size"xs" | "sm" | "base" | "lg""base"Controls group height, text size, spacing, and icon size.
disabledbooleanfalseDisables all child input and button controls.
requiredboolean-Set false to show "(optional)" after the label.
labelTooltipstring-Info text displayed next to the label on hover or focus.
inputIdstring-Overrides the generated id used to associate the label and input.

InputGroup.Input

The text input element. It inherits size, disabled, label, and error context from InputGroup.

PropTypeDefaultDescription
modelValuestring | number-Controlled value used by v-model.
defaultValuestring | number-Initial value for uncontrolled inputs.
typestring"text"Native input type.
placeholderstring-Native placeholder text.
disabledbooleanfalseDisables this input. The parent disabled prop also applies.
$attrsInputHTMLAttributes-Native input attributes such as aria-label, maxlength, name, and autocomplete.

InputGroup.Addon

Container for icons, text, or compact buttons positioned at the start or end of the input.

PropTypeDefaultDescription
align"start" | "end""start"Places the addon before or after the input.

InputGroup.Button

Button for secondary actions like toggle, clear, or help.

PropTypeDefaultDescription
variantButtonVariant"ghost"Button visual style. Direct child buttons can use secondary for action buttons.
shapeButtonShape"base"Use square for icon-only addon buttons.
iconComponent-Icon component rendered by the button.
tooltipstring-Hover/focus tooltip text. Also becomes aria-label when no label is provided.
tooltipSide"top" | "bottom" | "left" | "right""bottom"Preferred tooltip side.

InputGroup.Suffix

Inline text that flows seamlessly next to the typed value.

PropTypeDefaultDescription
default slotstring | VNode-Inline suffix text that follows the typed value.

Events

EventTypeDescription
update:modelValue(value: string) => voidEmitted by InputGroup.Input on native input for v-model.
valueChange(value: string) => voidString-only change event from InputGroup.Input.

Validation Error Types

Structured errors accept a browser validity match key.

Match
boolean
badInput
customError
patternMismatch
rangeOverflow
rangeUnderflow
stepMismatch
tooLong
tooShort
typeMismatch
valid
valueMissing

Accessibility

Label Requirement

Prefer the label prop on InputGroup. For bare groups, providearia-label or aria-labelledby on InputGroup.Input.

Group Role

InputGroup renders with role="group" to associate the input with its addons for assistive technologies.