Files
modrinth/standards/frontend/INPUTS.md
T
Calum H.andProspector c67f980c4a refactor: StyledInput refactor -> Input components (#6733)
* feat: align style

* feat: new input components

* refactor: migrate to new input components

* refactor: rename component

* prepr

* chore: group inputs like buttons

* fix: lint

* feat: docs

* fix: lint

* fix: bugs

* prepr

* fix slug + adjust create server project

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-08-25 06:15:38 +00:00

14 KiB

Inputs

Use the input components in packages/ui/src/components/base/inputs/. Use them for text, numbers, and dates. These components have the same sizes, focus style, appearances, disabled state, and error state.

Do not make input styles with raw <input> or <textarea> elements. Use a raw control only if a shared component does not have the necessary function.

InputFrame and InputClearButton are internal components. Do not import them directly into application code.

Select a component

Component Use
Input Single-line text, email, password, number, URL, or search entry
Textarea Multi-line text entry
DateInput A native browser control for a date or a local date and time
DatePicker A styled calendar for ranges, multiple dates, time, or inline presentation

Use a dedicated component for each other type of control. Do not use Input instead of a select, checkbox, radio group, toggle, or file picker.

Import the public input components from @modrinth/ui. In packages/ui, import them from #ui/components/base/inputs:

import { DateInput, DatePicker, Input, Textarea } from '@modrinth/ui'

Labels and field descriptions

The input components do not include a label, description, or validation message. Give each control an accessible name.

For a visible label, set the same value for the label for and the control id:

<label for="project-name">Project name</label>
<Input
	id="project-name"
	v-model="projectName"
	name="name"
	autocomplete="off"
/>

An input does not have a label prop. Do not use a placeholder as the only label. The placeholder is not visible after the user enters a value.

If a visible label repeats other content, use an accessible name such as aria-label:

<Input
	v-model="query"
	:icon="SearchIcon"
	type="search"
	aria-label="Search projects"
	placeholder="Search projects..."
/>

Connect descriptions and validation messages to the control with aria-describedby. The component sends additional attributes to the native control:

<label for="project-slug">Project URL</label>
<Input
	id="project-slug"
	v-model="slug"
	:error="Boolean(slugError)"
	aria-describedby="project-slug-help project-slug-error"
/>
<p id="project-slug-help">Use lowercase letters, numbers, and dashes.</p>
<p v-if="slugError" id="project-slug-error">
	{{ slugError }}
</p>

Use the localization system for all user-visible text. This requirement also applies to accessible labels.

Basic use

Bind the field value with v-model. For text types, the Input model is a string. For type="number", the model is a number or undefined:

<Input
	v-model="email"
	type="email"
	name="email"
	autocomplete="email"
	placeholder="name@example.com"
/>

<Input
	v-model="serverCount"
	type="number"
	:min="1"
	:max="100"
	:step="1"
	clamp
/>

An empty number input sets the model to undefined. The clamp prop keeps the number between min and max. Do not set clamp if validation must show the original value.

Set the native input type that agrees with the value. Set name and autocomplete if a form or autofill uses the field.

Set inputmode to select the correct virtual keyboard. Use it when the data format and the native input type are different.

Use Textarea for multi-line content:

<Textarea
	id="project-summary"
	v-model="summary"
	name="summary"
	:maxlength="500"
	:rows="4"
	resize="vertical"
/>

Textarea has three rows by default. The default value of resize is none. The other values are vertical and both.

Use vertical if the layout width must not change.

Input, Textarea, and DateInput have a focus() method. Use this method when a workflow must move keyboard focus:

const nameInput = ref<InstanceType<typeof Input> | null>(null)

nameInput.value?.focus()

Appearances

Input, Textarea, and DateInput have the same three appearances:

Appearance Use
surface Standard fields on pages, cards, and modals. This is the default.
button Compact fields in panels that use the button surface.
transparent Fields in a container that already shows the visible field border.
<Input v-model="name" appearance="surface" />
<Input v-model="filter" appearance="button" size="small" />
<Input v-model="value" appearance="transparent" />

Use transparent only if the parent container shows the field border. A field in the error state uses the standard red border and highlight for all appearances.

Set appearance to select the background and border. Do not use utility classes to make a different background or border.

DatePicker has only the standard surface appearance.

Sizes

Input and DateInput have four sizes. Select the size for the location. If a button is adjacent to the input, use the related button size:

Input size Height Button size Use
small 32 px sm Compact panels, toolbars, and popups
standard 36 px md Standard fields. This is the default.
medium 40 px lg Prominent modal, card, and header fields
large 48 px xl Page-level search and primary filters
<div class="flex items-center gap-2">
	<Input v-model="query" size="medium" class="flex-1" />
	<Button size="lg" type="colored">Search</Button>
</div>

Use the same size for fields in a group. Do not use classes to change the height, padding, border radius, font size, gap, or icon size.

Set rows or a layout class to change the Textarea height. DatePicker uses the standard input height.

Leading and trailing content

Set icon on Input to show a decorative leading icon:

<Input
	v-model="query"
	:icon="SearchIcon"
	type="search"
	placeholder="Search..."
/>

Use the leading slot for other decorative content. The component hides this content from assistive software. Do not put an interactive control or important status in this slot.

<Input v-model="domain">
	<template #leading>
		<LinkIcon />
	</template>
</Input>

Use the trailing slot for content that is part of the field. Give each interactive control an accessible name. Do not add a control that has the same function as the field:

<Input v-model="password" :type="showPassword ? 'text' : 'password'">
	<template #trailing>
		<IconButton
			type="quiet"
			:label="showPassword ? 'Hide password' : 'Show password'"
			@click="showPassword = !showPassword"
		>
			<EyeIcon aria-hidden="true" />
		</IconButton>
	</template>
</Input>

Use trailing in new code. The right slot exists only for compatibility with existing code.

Clearable inputs

Set clearable if users often clear an optional value. You can set it on Input, DateInput, and DatePicker.

The clear button is visible only when the field has a value. It is not visible when the field is disabled or readonly:

<Input
	v-model="query"
	type="search"
	clearable
	clear-label="Clear search"
	@clear="refreshResults"
/>

For Input and DateInput, set a localized clear-label. This label is the accessible name of the clear button. For DateInput, also set a localized picker-label for the calendar button.

After a clear action, a text Input or DateInput model is an empty string. A number Input model is undefined. A single DatePicker model is null. A multiple or range DatePicker model is an empty array.

The component sends a clear event after a clear action. Use this event only if the application must do an additional action. Other code can use the model change.

Do not add a second clear button to the trailing slot when clearable is set.

Disabled, readonly, and error states

Set disabled when the value is not available. A disabled control cannot receive focus and does not submit a value.

For Input, Textarea, and DateInput, set readonly when the user must not change the value. A readonly control can receive focus:

<Input v-model="generatedId" readonly />
<Input v-model="username" :disabled="accountLocked" />

Set error after validation finds an invalid value. Input, Textarea, and DateInput show the shared error style. They also set aria-invalid on the native control:

<Input
	id="website"
	v-model="website"
	type="url"
	:error="Boolean(websiteError)"
	aria-describedby="website-error"
/>
<p v-if="websiteError" id="website-error">{{ websiteError }}</p>

The error prop does not show a message and does not validate the value. Show a specific message. Connect the message to the field. Do not use only color to show an error state.

You can use native limits such as required, min, max, step, and maxlength. These limits do not set the error prop. Use application validation to control the visible error state.

DatePicker uses readonly for a different function. It prevents text entry, but it does not prevent calendar selection. Set disabled to prevent all changes.

Native date inputs

Use DateInput if the browser date control has the necessary function. The component accepts type="date" and type="datetime-local". The model contains the native date string:

<label for="scheduled-date">Scheduled date</label>
<DateInput
	id="scheduled-date"
	v-model="scheduledDate"
	type="datetime-local"
	:min="minimumDate"
	clearable
	clear-label="Clear scheduled date"
	picker-label="Open scheduled date picker"
/>

Set min, max, and step for native limits. The calendar button gives focus to the native input. It opens the native picker if the browser has showPicker().

Use DateInput for a native date control. Use DatePicker for one or more of these functions:

  • A calendar with the Modrinth style.
  • A date range or multiple dates.
  • Time controls.
  • More than one visible month.
  • An inline calendar.

Date picker

DatePicker uses a Flatpickr calendar. In single mode, its model is one value. In multiple and range modes, its model is an array. The component sends user selections as formatted strings:

<DatePicker
	v-model="releaseDate"
	placeholder="Select a release date..."
	:min-date="today"
	clearable
	close-on-select
/>

<DatePicker
	v-model="reportingRange"
	mode="range"
	:show-months="2"
	default-view-date="2026-01-01"
	placeholder="Select a reporting range..."
/>

The modes use these model types:

Mode Model type Value after clear
single string, Date, null, or undefined null
multiple An array of string or Date values []
range An array of string or Date start and end []

The stored format is Y-m-d by default. It is Y-m-d H:i when enable-time is set. The visible format is F j, Y by default. It is F j, Y at h:i K when enable-time is set.

Set date-format and alt-format to use other Flatpickr formats:

<DatePicker
	v-model="startsAt"
	enable-time
	time24hr
	date-format="Y-m-d H:i"
	alt-format="j M Y, H:i"
/>

Make sure that model strings agree with date-format. The alt-format prop changes only the visible value.

DatePicker sets readonly to true by default. The user selects a date from the calendar. Set :readonly="false" only when users can type a date.

Set close-on-select if a single selection must close the popup. Its default value is false.

Use the calendar options as follows:

  • Set default-view-date to select the month that opens when there is no value. This prop does not set the model.
  • Set view-date-alignment="right" to put the initial date in the right calendar. Use this prop when more than one month is visible.
  • Set min-date and max-date to limit the dates that a user can select.
  • Set show-today to highlight the current day. This prop does not select the day.
  • Set calendar-only to show an inline calendar without a visible input.
  • Set position to control the popup position and its alignment with the input.

Set preserve-day when a selection must keep the same day number during month changes. If the day does not exist, DatePicker uses the last day of that month. When the day exists again, DatePicker restores the original day.

The component sends a clamp event when it uses a different day. Use this event if the interface must explain the change:

<DatePicker
	v-model="renewalDate"
	preserve-day
	@clamp="showClampedDayMessage"
/>

The picker has focus(), open(), close(), and clear() methods. Use these methods only when a workflow requires control from the parent component.

Classes and native attributes

Input, Textarea, and DateInput send additional attributes to the native control. Examples are required, aria-label, and aria-describedby. The outer frame receives class and style.

Set wrapper-class for field width and parent layout. Set input-class only when the native control needs a layout adjustment:

<Input v-model="name" wrapper-class="w-full" />

The input-attrs prop is for input wrapper components. Application components must pass native attributes directly.

Use the shared styles for borders, backgrounds, focus rings, height, padding, text, gaps, and icon sizes. Do not replace these styles with utility classes. Width and parent layout classes are permitted.

DatePicker also has a calendar-class prop. Use it for layout or state on the calendar. Do not use it to replace the standard calendar styles.