Form
A form allows people to input information.Building a form
The form component is a wrapper that doesn't render anything itself. Child elements must be added for it to function as a form.
The form component passes props and information down into the <form> element and its children.
This includes information about whether the form is dirty, disabled, reset or submitting.
Each form needs a header and footer, and form sections can be added to the body of a form. 'Field' is used as a container for form fields and 'Fieldset' groups related fields and form controls.
People can submit a form when all fields are valid. Submitting the form calls the 'onSubmit' function.
import React, { Fragment } from 'react';
import ButtonGroup from '@atlaskit/button/button-group';
import Button from '@atlaskit/button/default/button';
import Field from '@atlaskit/form/field';
import Form from '@atlaskit/form/form';
import { FormFooter } from '@atlaskit/form/form-footer';
import { FormHeader } from '@atlaskit/form/form-header';
import { FormSection } from '@atlaskit/form/form-section';
import { ErrorMessage } from '@atlaskit/form/error-message';
import { HelperMessage } from '@atlaskit/form/helper-message';
import { MessageWrapper } from '@atlaskit/form/message-wrapper';
import { ValidMessage } from '@atlaskit/form/valid-message';
import { RequiredAsterisk } from '@atlaskit/form/required-asterisk';
import { Flex } from '@atlaskit/primitives/compiled/flex';
import RadioGroup from '@atlaskit/radio/radio-group';
import TextField from '@atlaskit/textfield/text-field';
const FormDefaultExample = (): React.JSX.Element => (
<Flex direction="column">
<Form<{ schema: string; key: string; type: string }>
noValidate
onSubmit={(data) => {
console.log('form data', data);
return new Promise((resolve) => setTimeout(resolve, 2000)).then(() =>
!data.schema ? { schema: 'A schema name is required' } : undefined,
);
}}
>
{({ formProps, submitting }) => (
<form {...formProps} name="create">
<FormHeader title="Create schema">
<p aria-hidden="true">
Required fields are marked with an asterisk <RequiredAsterisk />
</p>
</FormHeader>
<FormSection>
<Field
name="schema"
label="Schema name"
isRequired
defaultValue=""
validate={(value) => (!value ? 'A schema name is required' : undefined)}
>
{({ fieldProps, error }) => {
return (
<Fragment>
<TextField autoComplete="off" {...fieldProps} />
<MessageWrapper>{error && <ErrorMessage>{error}</ErrorMessage>}</MessageWrapper>
</Fragment>
);
}}
</Field>
<Field
name="key"
label="Key"
defaultValue=""
isRequired
validate={(value) => {
if (!value) {
return 'A key is required';
}
if (value.length < 8) {
return 'Key needs to be at least 8 characters.';
}
}}
>
{({ fieldProps, error, valid, meta }) => {
return (
<Fragment>
<TextField type="key" {...fieldProps} />
<MessageWrapper>
<HelperMessage>
Create a unique key, minimum of 8 characters. Example key: IT-infrastructure
</HelperMessage>
{error && <ErrorMessage>{error}</ErrorMessage>}
{valid && meta.dirty ? <ValidMessage>Key is unique</ValidMessage> : null}
</MessageWrapper>
</Fragment>
);
}}
</Field>
<Field
name="type"
defaultValue=""
label="Schema type"
component={({ fieldProps }) => (
<RadioGroup
options={[
{
name: 'type',
value: 'project-admin',
label: 'Public',
},
{
name: 'type',
value: 'admin',
label: 'Private',
},
]}
{...fieldProps}
/>
)}
/>
</FormSection>
<FormFooter align="start">
<ButtonGroup label="Form submit options">
<Button type="submit" appearance="primary">
Create
</Button>
<Button appearance="subtle" isLoading={submitting}>
Cancel
</Button>
</ButtonGroup>
</FormFooter>
</form>
)}
</Form>
</Flex>
);
export default FormDefaultExample;Simple implementation of a form
If your form component only requires spreading formProps on the HTML <form> element and doesn't
utilize any of the other render props like submitting, you can simplify how it's written in your
code.
Props can be added to the underlying <form> element by applying valid props to the atlaskit Form
component. If the prop is not directly supported by the atlaskit Form component, you can add any
other props to formProps.
In the simplified form, all of the form component's children are already wrapped within an HTML
<form> element, including all necessary props as well as those provided on the form component.
import React from 'react';
import ButtonGroup from '@atlaskit/button/button-group';
import Button from '@atlaskit/button/default/button';
import { Checkbox } from '@atlaskit/checkbox/checkbox';
import Form from '@atlaskit/form/form';
import { CheckboxField } from '@atlaskit/form/checkbox-field';
import Field from '@atlaskit/form/field';
import { FormFooter } from '@atlaskit/form/form-footer';
import { FormHeader } from '@atlaskit/form/form-header';
import { FormSection } from '@atlaskit/form/form-section';
import { RequiredAsterisk } from '@atlaskit/form/required-asterisk';
import { Flex } from '@atlaskit/primitives/compiled';
import TextField from '@atlaskit/textfield/text-field';
const FormDefaultExample = (): React.JSX.Element => (
<Flex direction="column">
<Form<{ schema: string; key: string; private: boolean }>
onSubmit={(data) => {
console.log('form data', data);
}}
noValidate
name="create"
formProps={{ 'data-attribute': 'example' }}
>
<FormHeader title="Create schema">
<p aria-hidden="true">
Required fields are marked with an asterisk <RequiredAsterisk />
</p>
</FormHeader>
<FormSection>
<Field
name="schema"
label="Schema name"
defaultValue=""
isRequired
validate={(value) => (!value ? 'A schema name is required' : undefined)}
component={({ fieldProps }) => <TextField {...fieldProps} />}
/>
<Field
name="key"
label="Key"
defaultValue=""
isRequired
helperMessage="Create a unique key, minimum of 8 characters. Example key: IT-infrastructure"
validMessage="Key is valid"
component={({ fieldProps }) => <TextField autoComplete="off" {...fieldProps} />}
validate={(value) => {
if (!value) {
return 'A key is required';
}
if (value.length < 8) {
return 'Enter a minimum of 8 characters.';
}
}}
/>
<CheckboxField name="private">
{({ fieldProps }) => <Checkbox {...fieldProps} label="Private schema" />}
</CheckboxField>
</FormSection>
<FormFooter align="start">
<ButtonGroup label="Form submit options">
<Button type="submit" appearance="primary">
Create
</Button>
<Button appearance="subtle">Cancel</Button>
</ButtonGroup>
</FormFooter>
</Form>
</Flex>
);
export default FormDefaultExample;Form structure
Layout
The layout of a form is made up of 3 main areas: header, section, and footer.
Form header
A form header includes the title and an optional description of the form content.
If your form contains required fields, the form header should include the legend 'Required fields
are marked with an asterisk *' so sighted users know that an asterisk * indicates a required
field.
Form section
Use a form section to group related information together and make longer forms easier to understand.
Form section is a higher-level wrapper than Fieldset.
An optional description can be added to a section and multiple form sections can be used in one form.
Use a form footer to set buttons at the end of the form. The footer is positioned after the form's last field.
Alignment
- Left-aligned: buttons align to the left on single page and multi-page forms. The primary button sits to the left of the secondary button.
- Right-aligned: align buttons on the right for forms in a modal dialog. A primary buttons sits to the right of the secondary button.
- Center-aligned: for log in/sign-in forms, buttons are full width and center-aligned.
import React from 'react';
import ButtonGroup from '@atlaskit/button/button-group';
import Button from '@atlaskit/button/default/button';
import { Checkbox } from '@atlaskit/checkbox/checkbox';
import Form from '@atlaskit/form/form';
import { CheckboxField } from '@atlaskit/form/checkbox-field';
import Field from '@atlaskit/form/field';
import { FormFooter } from '@atlaskit/form/form-footer';
import { FormHeader } from '@atlaskit/form/form-header';
import { FormSection } from '@atlaskit/form/form-section';
import { RequiredAsterisk } from '@atlaskit/form/required-asterisk';
import { Flex } from '@atlaskit/primitives/compiled';
import RadioGroup from '@atlaskit/radio/radio-group';
import Select from '@atlaskit/select/default';
import type { OptionType, ValueType } from '@atlaskit/select/types';
import Textfield from '@atlaskit/textfield/text-field';
const FormLayoutExample = (): React.JSX.Element => {
return (
<Flex direction="column">
<Form
onSubmit={console.log}
name="create-repo"
formProps={{
action: '//httpbin.org/get',
method: 'GET',
target: 'submitFrame',
}}
>
<FormHeader title="Create a new repository">
<p>A repository is the central hub for managing and collaborating on your project.</p>
<p aria-hidden="true">
Required fields are marked with an asterisk <RequiredAsterisk />
</p>
</FormHeader>
<FormSection>
<Field<ValueType<OptionType>> label="Owner" name="owner" id="owner">
{({ fieldProps: { id, ...rest } }) => (
<Select
placeholder=""
id={`${id}-select`}
isSearchable={false}
options={[
{ label: 'Arni Singh', value: 'asingh' },
{ label: 'Hermione Walters', value: 'hwalters' },
{ label: 'Parvi Karan', value: 'pkaran' },
{ label: 'Charles Li', value: 'cli' },
]}
{...rest}
/>
)}
</Field>
<Field<ValueType<OptionType>>
name="app"
id="app"
label="App"
isRequired
component={({ fieldProps: { id, ...rest } }) => (
<Select
placeholder=""
id={`${id}-select`}
options={[
{ label: 'Atlaskit', value: 'atlaskit' },
{ label: 'Bitbucket', value: 'bitbucket' },
{ label: 'Confluence', value: 'confluence' },
{ label: 'Jira', value: 'jira' },
]}
{...rest}
/>
)}
/>
<Field
name="repo-name"
label="Repository name"
defaultValue=""
isRequired
component={({ fieldProps }) => <Textfield {...fieldProps} />}
/>
<CheckboxField name="readme-file" label="README file">
{({ fieldProps }) => <Checkbox label="Include a README file" {...fieldProps} />}
</CheckboxField>
<Field
name="repository"
label="Repository type"
component={({ fieldProps: { value, ...others } }) => (
<RadioGroup
options={[
{ name: 'repository', value: 'public', label: 'Public' },
{
name: 'repository',
value: 'private',
label: 'Private',
},
]}
value={value}
{...others}
/>
)}
/>
</FormSection>
<FormFooter align="start">
<ButtonGroup label="Form submit options">
<Button appearance="primary" id="create-repo-cancel" type="submit">
Create
</Button>
<Button appearance="subtle" id="create-repo-button">
Cancel
</Button>
</ButtonGroup>
</FormFooter>
</Form>
</Flex>
);
};
export default FormLayoutExample;Field
Field allows for an entry in the form. It comes with props that give more information about the
field state, which can be passed on to the inner component.
CheckboxField, RangeField, and
CharacterCounterField are part of the form package.
When a person focuses on a Field and starts changing content, the focus color becomes blue.
import React from 'react';
import ButtonGroup from '@atlaskit/button/button-group';
import Button from '@atlaskit/button/default/button';
import Form from '@atlaskit/form/form';
import Field from '@atlaskit/form/field';
import { FormFooter } from '@atlaskit/form/form-footer';
import { FormHeader } from '@atlaskit/form/form-header';
import { FormSection } from '@atlaskit/form/form-section';
import { Flex, Text } from '@atlaskit/primitives/compiled';
import TextField from '@atlaskit/textfield/text-field';
const FormFieldExample = (): React.JSX.Element => (
<Flex direction="column">
<Form onSubmit={(data) => console.log('form data', data)}>
{({ formProps, submitting }) => (
<form {...formProps}>
<FormHeader title="Archive page"></FormHeader>
<Text as="p">Add an optional note to say why this page was archived.</Text>
<FormSection>
<Field name="note" defaultValue="" label="Note">
{({ fieldProps }) => (
<>
<TextField {...fieldProps} />
</>
)}
</Field>
</FormSection>
<FormFooter>
<ButtonGroup label="Form submit options">
<Button appearance="subtle">Cancel</Button>
<Button type="submit" appearance="primary" isLoading={submitting}>
Archive
</Button>
</ButtonGroup>
</FormFooter>
</form>
)}
</Form>
</Flex>
);
export default FormFieldExample;Simplified Field
If your field only requires spreading fieldProps on the input element and doesn't use any other
render props like meta, you can simplify how it's written in your code by putting the contents of
your field within the component prop and omitting the messaging component (for example,
ErrorMessage).
This simplified implementation will provide greater accessibility and more consistent styling through rendering components of your field internally.
If an error is returned by the function provided in the validate prop, it will be rendered
automatically in an internal ErrorMessage component. Content that would previously go in the
HelperMessage and ValidMessage components will be rendered when provided using the
helperMessage and validMessage props.
If you need more control of how messaging or labeling renders, don't use this simple implementation
of a Field.
import React from 'react';
import ButtonGroup from '@atlaskit/button/button-group';
import Button from '@atlaskit/button/default/button';
import Field from '@atlaskit/form/field';
import Form from '@atlaskit/form/form';
import { FormFooter } from '@atlaskit/form/form-footer';
import { Flex } from '@atlaskit/primitives/compiled/flex';
import TextField from '@atlaskit/textfield/text-field';
const FormFieldExample = (): React.JSX.Element => (
<Flex direction="column">
<Form onSubmit={(data) => console.log('form data', data)}>
{({ formProps }) => (
<form {...formProps}>
<Field
name="username"
defaultValue=""
label="Username"
isRequired
helperMessage="Your username can have up to 16 characters."
validMessage="Username is valid."
validate={(value) => {
if (!value) {
return 'Username is required.';
} else if (value && value.length > 16) {
return 'Username must be 16 characters or less.';
}
}}
component={({ fieldProps }) => <TextField {...fieldProps} />}
/>
<FormFooter align="start">
<ButtonGroup label="Form submit options">
<Button type="submit" appearance="primary">
Submit
</Button>
<Button appearance="subtle">Cancel</Button>
</ButtonGroup>
</FormFooter>
</form>
)}
</Form>
</Flex>
);
export default FormFieldExample;Fieldset
Use a Fieldset element to group related fields under a heading. It's useful to group components
like checkboxes and text inputs that are related, like the fields that make up an address.
Use legend to assign a caption to the Fieldset, as this improves accessibility for when the
Fieldset is rendered non-visually for screen readers.
import React from 'react';
import { Checkbox } from '@atlaskit/checkbox/checkbox';
import { CheckboxField } from '@atlaskit/form/checkbox-field';
import { Fieldset } from '@atlaskit/form/fieldset';
import Form from '@atlaskit/form/form';
import { Box } from '@atlaskit/primitives/compiled/box';
const FormFieldsetExample = (): React.JSX.Element => (
<Box>
<Form onSubmit={(data) => console.log(data)}>
<Fieldset legend="Apps">
<CheckboxField name="app" value="jira">
{({ fieldProps }) => <Checkbox {...fieldProps} label="Jira" />}
</CheckboxField>
<CheckboxField name="app" value="confluence">
{({ fieldProps }) => <Checkbox {...fieldProps} label="Confluence" />}
</CheckboxField>
<CheckboxField name="app" value="bitbucket">
{({ fieldProps }) => <Checkbox {...fieldProps} label="Bitbucket" />}
</CheckboxField>
</Fieldset>
<Fieldset legend="Teams">
<CheckboxField name="teams" value="dst">
{({ fieldProps }) => <Checkbox {...fieldProps} label="Design System Team" />}
</CheckboxField>
<CheckboxField name="teams" value="design-ops">
{({ fieldProps }) => <Checkbox {...fieldProps} label="Design Ops" />}
</CheckboxField>
<CheckboxField name="teams" value="content">
{({ fieldProps }) => <Checkbox {...fieldProps} label="Content Ops" />}
</CheckboxField>
</Fieldset>
</Form>
</Box>
);
export default FormFieldsetExample;CheckboxField
CheckboxField lets people select one or more options from a number of choices. It should have a
label prop that renders the label inline with the checkbox.
By default, the value of a CheckboxField is true or false. Use the value prop to pass a
value when the field is checked. This will return an array that contains value.
Grouping CheckboxField
When grouping checkboxes using Fieldset, they should all have the same name prop value so
they're grouped properly. For example, in a Fieldset that allows multiple selections, submitting
the form will show the chosen options as the value of that field.
import React from 'react';
import Button from '@atlaskit/button/default/button';
import { Checkbox } from '@atlaskit/checkbox/checkbox';
import { CheckboxField } from '@atlaskit/form/checkbox-field';
import { Fieldset } from '@atlaskit/form/fieldset';
import Form from '@atlaskit/form/form';
import { FormFooter } from '@atlaskit/form/form-footer';
import { Flex } from '@atlaskit/primitives/compiled/flex';
const FormCheckboxExample = (): React.JSX.Element => {
return (
<Flex direction="column">
<Form onSubmit={(data) => console.log(data)}>
<Fieldset legend="Apps">
<CheckboxField name="app" value="jira">
{({ fieldProps }) => <Checkbox {...fieldProps} label="Jira" />}
</CheckboxField>
<CheckboxField name="app" value="confluence">
{({ fieldProps }) => <Checkbox {...fieldProps} label="Confluence" />}
</CheckboxField>
<CheckboxField name="app" value="bitbucket">
{({ fieldProps }) => <Checkbox {...fieldProps} label="Bitbucket" />}
</CheckboxField>
</Fieldset>
<FormFooter align="start">
<Button type="submit" appearance="primary">
Submit
</Button>
</FormFooter>
</Form>
</Flex>
);
};
export default FormCheckboxExample;RangeField
RangeField allows people to choose an approximate value on a slider.
It requires defaultValue to set the initial state, as well as a name prop so that it defines the
RangeField for form submission.
Default values for the range component are from 0 to 100 with increments of 1. This can be adjusted
by specifying the min, max, and step props.
import React from 'react';
import Button from '@atlaskit/button/default/button';
import Form from '@atlaskit/form/form';
import { FormFooter } from '@atlaskit/form/form-footer';
import { RangeField } from '@atlaskit/form/range-field';
import { Box } from '@atlaskit/primitives/compiled/box';
import Range from '@atlaskit/range/range';
const FormRangeFieldExample = (): React.JSX.Element => {
return (
<Box>
<Form onSubmit={(data) => console.log(data)}>
<RangeField name="threshold" defaultValue={50} label="Threshold">
{({ fieldProps }) => <Range {...fieldProps} min={0} max={70} />}
</RangeField>
<FormFooter align="start">
<Button type="submit" appearance="primary">
Submit
</Button>
</FormFooter>
</Form>
</Box>
);
};
export default FormRangeFieldExample;CharacterCounterField
A character counter field provides real-time feedback about text length as people type, showing how many characters are remaining or have been exceeded.
Use it when there are constraints on text length, such as database limits, technical requirements, or design constraints.
Configure minimum length with minCharacters and maximum length with maxCharacters. With these
props, the counter automatically updates its message and styling based, showing either:
- characters needed when under the minimum
- remaining characters within range
- error states when limits are exceeded
This field is more accessible than using native HTML maxLength and minLength attributes, as it
provides clear visual feedback and screen reader announcements about character count changes,
helping users stay within limits before form submission.
Feedback messages and validation
You can customize feedback messages using the underMinimumMessage,underMaximumMessage, and
overMaximumMessage props.
For validation beyond character limits, combine the character counter logic with the validate prop
to implement custom validation rules (such as checking for specific patterns, forbidden characters,
or business logic requirements).
import React from 'react';
import ButtonGroup from '@atlaskit/button/button-group';
import Button from '@atlaskit/button/default/button';
import { CharacterCounterField } from '@atlaskit/form/character-counter-field';
import { type FieldProps } from '@atlaskit/form/field';
import Form from '@atlaskit/form/form';
import { FormFooter } from '@atlaskit/form/form-footer';
import { FormHeader } from '@atlaskit/form/form-header';
import { FormSection } from '@atlaskit/form/form-section';
import { RequiredAsterisk } from '@atlaskit/form/required-asterisk';
import { Flex } from '@atlaskit/primitives/compiled/flex';
import { Text } from '@atlaskit/primitives/compiled/text';
import TextArea from '@atlaskit/textarea/text-area';
import TextField from '@atlaskit/textfield/text-field';
/**
* Mock i18n setup - in a real app, these would come from your i18n library
* Example: import { useIntl } from 'react-intl';
*/
const messages = {
'bio.underMinimum': 'Enter at least {minimum} characters.',
'bio.overMaximum': 'Your bio exceeds the maximum length of {maximum} characters',
};
// Mock formatMessage - in a real app: const { formatMessage } = useIntl();
const formatMessage = (
messageDescriptor: { id: keyof typeof messages },
values?: Record<string, string | number>,
): string => {
let message = messages[messageDescriptor.id];
if (values) {
Object.entries(values).forEach(([key, value]) => {
message = message.replace(new RegExp(`\\{${key}\\}`, 'g'), String(value));
});
}
return message;
};
const FormCharacterCounterExample = (): React.JSX.Element => (
<Flex direction="column">
<Form
noValidate
onSubmit={(data) => {
console.log('form data', data);
}}
>
<FormHeader title="Profile">
<Text as="p" aria-hidden="true">
Required fields are marked with an asterisk <RequiredAsterisk />
</Text>
</FormHeader>
<FormSection>
{/* Example 1: Maximum characters only with default messages */}
<CharacterCounterField
name="displayName"
label="Display name"
isRequired
maxCharacters={50}
helperMessage="The name you’d like other people to see."
validate={(value) =>
value === 'Atlas' ? 'Atlas is already in use, try something else' : undefined
}
>
{({ fieldProps }: { fieldProps: FieldProps<string> }) => (
<TextField autoComplete="name" {...fieldProps} />
)}
</CharacterCounterField>
{/* Example 2: Minimum characters only with default messages */}
<CharacterCounterField<string, HTMLTextAreaElement>
name="tagline"
label="Professional tagline"
minCharacters={10}
helperMessage="A short headline that describes what you do."
>
{({ fieldProps }) => <TextArea {...fieldProps} resize="auto" minimumRows={2} />}
</CharacterCounterField>
{/* Example 3: Using i18n messages with character counter */}
<CharacterCounterField<string, HTMLTextAreaElement>
name="bio"
label="Bio"
isRequired
minCharacters={10}
maxCharacters={200}
helperMessage="Tell us about yourself, your interests, and experience."
underMinimumMessage={formatMessage({ id: 'bio.underMinimum' }, { minimum: 10 })}
overMaximumMessage={formatMessage({ id: 'bio.overMaximum' }, { maximum: 200 })}
>
{({ fieldProps }) => <TextArea {...fieldProps} resize="auto" minimumRows={3} />}
</CharacterCounterField>
</FormSection>
<FormFooter align="start">
<ButtonGroup label="Form submit options">
<Button type="submit" appearance="primary">
Save profile
</Button>
<Button appearance="subtle">Cancel</Button>
</ButtonGroup>
</FormFooter>
</Form>
</Flex>
);
export default FormCharacterCounterExample;Standalone CharacterCounter
The CharacterCounter component can also be used for custom implementations. This is useful when
you're building custom form layout experiences or need character counting in non-form contexts. Do
note this should generally not be used and instead use CharacterCounterField for better
consistency across forms.
When using standalone, you're responsible for:
- Managing the input's value state
- Passing
currentValueto keep the counter in sync - Providing an
inputIdto link the counter with the input viaaria-describedby - Controlling error message styling with
shouldShowAsErrorbased on your validation logic
import React, { useState } from 'react';
import { CharacterCounter } from '@atlaskit/form/character-counter';
import { Label } from '@atlaskit/form/label/default';
import { Box } from '@atlaskit/primitives/compiled/box';
import { Stack } from '@atlaskit/primitives/compiled/stack';
import TextArea from '@atlaskit/textarea/text-area';
import TextField from '@atlaskit/textfield/text-field';
/**
* Standalone CharacterCounter example - used outside of Form context
* This is useful when you need character counting in custom implementations
* that don't use the Form component or have a specific layout requirements
* that CharacterCounterField does not provide. Generally speaking, it is
* recommended to use CharacterCounterField for consistent styling.
*/
const StandaloneCharacterCounterExample = (): React.JSX.Element => {
const [textFieldValue, setTextFieldValue] = useState('');
const [textAreaValue, setTextAreaValue] = useState('');
const textFieldId = 'standalone-text-field';
const textAreaId = 'standalone-text-area';
// Character limits
const maxCharacters = 50;
const minCharacters = 10;
const textAreaMaxCharacters = 200;
// Calculate error states for styling
const isTextFieldTooLong = textFieldValue.length > maxCharacters;
const isTextAreaTooShort = textAreaValue.length < minCharacters;
const isTextAreaTooLong = textAreaValue.length > textAreaMaxCharacters;
const hasTextAreaError = isTextAreaTooShort || isTextAreaTooLong;
return (
<Stack space="space.200">
{/* Example 1: TextField with maximum character limit */}
<Box>
<Label htmlFor={textFieldId}>Display name</Label>
<TextField
id={textFieldId}
value={textFieldValue}
onChange={(e) => setTextFieldValue(e.currentTarget.value)}
aria-describedby={`${textFieldId}-character-counter`}
isInvalid={isTextFieldTooLong}
/>
<CharacterCounter
currentValue={textFieldValue}
maxCharacters={maxCharacters}
inputId={textFieldId}
shouldShowAsError={isTextFieldTooLong}
/>
</Box>
{/* Example 2: TextArea with both minimum and maximum limits */}
<Box>
<Label htmlFor={textAreaId}>Bio</Label>
<TextArea
id={textAreaId}
value={textAreaValue}
onChange={(e) => setTextAreaValue(e.currentTarget.value)}
aria-describedby={`${textAreaId}-character-counter`}
resize="auto"
minimumRows={3}
isInvalid={hasTextAreaError}
isRequired
/>
<CharacterCounter
currentValue={textAreaValue}
minCharacters={minCharacters}
maxCharacters={textAreaMaxCharacters}
inputId={textAreaId}
shouldShowAsError={hasTextAreaError}
/>
</Box>
</Stack>
);
};
export default StandaloneCharacterCounterExample;Form fields
Fields in a form are made up of components and rendered by using field. Any component with a value
and onChange handler can be a field. The component renders inside a field and adds an entry to the
form state.
Text field
A text field component is a space for people to write or edit text. Use for text that spans one line.
At a minimum, use a name prop to provide the form component with the name of the field. The props
allowed on a text field also
extend the native HTML input element.
Text area field
When there's a need for long-form plain text that spans multiple lines, use a text area component.
Select field
A select component allows people to make single or multiple selections from a dropdown list of options.
To show options in the list, the select component needs the passing in of listed options in the
options prop. An empty options prop will render the words 'No options' in the drop down.
import React from 'react';
import Button from '@atlaskit/button/default/button';
import Form from '@atlaskit/form/form';
import Field from '@atlaskit/form/field';
import { FormFooter } from '@atlaskit/form/form-footer';
import { Flex } from '@atlaskit/primitives/compiled';
import Select from '@atlaskit/select/default';
import type { ValueType as Value } from '@atlaskit/select/types';
interface Option {
label: string;
value: string;
}
interface Category {
type?: Value<Option>;
owner?: Value<Option[]>;
suit?: Value<Option[]>;
}
const types = [
{ label: 'Library', value: 'library' },
{ label: 'Application', value: 'application' },
{ label: 'Capability', value: 'capability' },
{ label: 'Cloud resource', value: 'cloud resource' },
{ label: 'Data pipeline', value: 'data pipeline' },
{ label: 'Machine learning model', value: 'Mmchine learning model' },
{ label: 'UI element', value: 'ui element' },
];
const owners = [
{ label: 'Design System Team', value: 'Design System Team' },
{ label: 'Accessibility', value: 'Accessibility' },
{ label: 'Design Ops', value: 'Design Ops' },
{ label: 'Experience', value: 'Experience' },
];
const status = [
{ label: 'To do', value: 'to do' },
{ label: 'In progress', value: 'in progress' },
{ label: 'In review', value: 'in review' },
{ label: 'Done', value: 'done' },
];
const validateOnSubmit = (data: Category) => {
let errors;
errors = typeValidation(data, errors);
errors = ownerValidation(data, errors);
return errors;
};
const typeValidation = (data: Category, errors?: Record<string, string>) => {
if (data.type && !(data.type instanceof Array)) {
return (data.type as Option).value === 'dog'
? {
...errors,
type: `${(data.type as Option).value} is not a type`,
}
: errors;
}
return errors;
};
const ownerValidation = (data: Category, errors?: Record<string, string>) => {
if (data.owner && data.owner.length >= 2) {
return {
...errors,
owner: `${data.owner.length} is too many owners. Select a maximum of 1 owner.`,
};
}
return errors;
};
const FormSelectExample = (): React.JSX.Element => {
return (
<Flex direction="column">
<Form<Category>
onSubmit={(data) => {
console.log('form data', data);
return Promise.resolve(validateOnSubmit(data));
}}
>
<Field<Value<Option>>
name="type"
label="Type"
defaultValue={null}
component={({ fieldProps: { id, ...rest } }) => (
<Select<Option>
inputId={id}
{...rest}
options={types}
isClearable
clearControlLabel="Clear type"
/>
)}
/>
<Field<Value<Option, true>>
name="owner"
label="Owner"
defaultValue={[]}
component={({ fieldProps: { id, ...rest } }) => (
<Select inputId={id} {...rest} options={owners} isMulti />
)}
/>
<Field<Value<Option, true>>
name="status"
label="Status"
defaultValue={status.slice(2)}
component={({ fieldProps: { id, ...rest } }) => (
<Select inputId={id} {...rest} options={status} isMulti />
)}
/>
<FormFooter align="start">
<Button type="submit" appearance="primary">
Submit
</Button>
</FormFooter>
</Form>
</Flex>
);
};
export default FormSelectExample;Radio field
Use a radio component if people need to choose only one option from a number of choices.
Provide the label prop to add an inline label. Provide the value prop to define the value that
is submitted to the form.
Grouping radios
To group radios, wrap them in a radio group component to semantically indicate the radios are together.
To ensure correct grouping, either use the name prop on the radio group component or make sure all
radios within the group have the same name. Failure to do so will cause incorrect grouping.
Date time picker field
A date time picker component lets someone easily select a date and/or time.
To define specific props for the underlying pickers (date and time), use datePicker props and
timePicker props.
import React, { Fragment } from 'react';
import Button from '@atlaskit/button/default/button';
import DatePicker from '@atlaskit/datetime-picker/date-picker';
import DateTimePicker from '@atlaskit/datetime-picker/date-time-picker';
import Form from '@atlaskit/form/form';
import { ErrorMessage } from '@atlaskit/form/error-message';
import Field from '@atlaskit/form/field';
import { FormFooter } from '@atlaskit/form/form-footer';
import { MessageWrapper } from '@atlaskit/form/message-wrapper';
import { Flex } from '@atlaskit/primitives/compiled';
interface FormData {
DOB: string;
preference: string;
}
const validateOnSubmit = (data: FormData) => {
let errors: Record<string, string> = {};
errors = dobValidator(data, errors);
errors = preferenceValidator(data, errors);
return errors;
};
const dobValidator = (data: FormData, errors: Record<string, string>) => {
if (!data.DOB) {
return {
...errors,
DOB: `Select a date of birth.`,
};
}
return errors;
};
const preferenceValidator = (data: FormData, errors: Record<string, string>) => {
if (!data.preference) {
return {
...errors,
preference: `Select an appointment date and time.`,
};
}
return errors;
};
const FormDateTimePickerExample = (): React.JSX.Element => {
return (
<Flex direction="column">
<Form<FormData>
onSubmit={(data) => {
console.log('form data', data);
return Promise.resolve(validateOnSubmit(data));
}}
>
<Field
name="DOB"
label="Date of birth"
defaultValue=""
isRequired
component={({ fieldProps }) => <DatePicker shouldShowCalendarButton {...fieldProps} />}
/>
<Field
name="preference"
label="Preferred appointment date and time"
defaultValue="2025-11-01"
isRequired
>
{({ fieldProps: { id, ...rest }, error }) => {
const validationState = error ? 'error' : 'none';
return (
<Fragment>
<DateTimePicker
{...rest}
datePickerProps={{
shouldShowCalendarButton: true,
selectProps: { validationState },
label: 'Date, Preferred appointment date and time',
id,
}}
timePickerProps={{
selectProps: { validationState },
label: 'Time, Preferred appointment date and time',
}}
/>
<MessageWrapper>{error && <ErrorMessage>{error}</ErrorMessage>}</MessageWrapper>
</Fragment>
);
}}
</Field>
<FormFooter align="start">
<Button type="submit" appearance="primary">
Submit
</Button>
</FormFooter>
</Form>
</Flex>
);
};
export default FormDateTimePickerExample;Toggle field
A toggle component allows someone to turn something on or off.
Include a visible label with the toggle. When there isn't a visible label you can pair a toggle
with, use the label prop to tell people who use assistive technology what the toggle is for.
Form with all fields and labels
This example shows you how a form could look with every possible input included and with required fields.
import React from 'react';
import ButtonGroup from '@atlaskit/button/button-group';
import Button from '@atlaskit/button/default/button';
import { Checkbox } from '@atlaskit/checkbox/checkbox';
import DateTimePicker from '@atlaskit/datetime-picker/date-time-picker';
import noop from '@atlaskit/ds-lib/noop';
import Form from '@atlaskit/form/form';
import { CheckboxField } from '@atlaskit/form/checkbox-field';
import Field from '@atlaskit/form/field';
import { Fieldset } from '@atlaskit/form/fieldset';
import { FormFooter } from '@atlaskit/form/form-footer';
import { FormHeader } from '@atlaskit/form/form-header';
import { FormSection } from '@atlaskit/form/form-section';
import { Label } from '@atlaskit/form/label/default';
import { RangeField } from '@atlaskit/form/range-field';
import { RequiredAsterisk } from '@atlaskit/form/required-asterisk';
import { Box, Flex } from '@atlaskit/primitives/compiled';
import RadioGroup from '@atlaskit/radio/radio-group';
import Range from '@atlaskit/range/range';
import Select from '@atlaskit/select/default';
import type { OptionType, ValueType } from '@atlaskit/select/types';
import TextArea from '@atlaskit/textarea/text-area';
import TextField from '@atlaskit/textfield/text-field';
import Toggle from '@atlaskit/toggle';
const FormAllOptionsExample = (): React.JSX.Element => (
<Flex direction="column">
<Form onSubmit={noop}>
{({ formProps, submitting }) => (
<form noValidate {...formProps}>
<FormHeader title="Form header">
<p aria-hidden="true">
Required fields are marked with an asterisk <RequiredAsterisk />
</p>
</FormHeader>
<FormSection>
<Field
name="textfield-name"
label="Text field label"
isRequired
defaultValue=""
helperMessage="This is helper text."
component={({ fieldProps }) => <TextField autoComplete="off" {...fieldProps} />}
/>
<Field
name="textarea-field-name"
label="Text area field label"
isRequired
component={({ fieldProps }: any) => <TextArea {...fieldProps} />}
/>
<Fieldset legend="Checkbox fieldset label">
<CheckboxField name="box" value="option1">
{({ fieldProps }) => <Checkbox {...fieldProps} label="Option 1" />}
</CheckboxField>
<CheckboxField name="box" value="option2">
{({ fieldProps }) => <Checkbox {...fieldProps} label="Option 2" />}
</CheckboxField>
<CheckboxField name="box" value="option3">
{({ fieldProps }) => <Checkbox {...fieldProps} label="Option 3" />}
</CheckboxField>
</Fieldset>
<Fieldset legend="Date time picker label">
{/* This label uses a legend because datetime picker has two fields in it. */}
<Field
name="datetime-picker-accessible"
isRequired
component={({ fieldProps: { id, ...rest } }) => (
<DateTimePicker
{...rest}
datePickerProps={{
shouldShowCalendarButton: true,
label: 'Select date',
id: id,
}}
timePickerProps={{
label: 'Select time',
}}
/>
)}
/>
</Fieldset>
<RangeField name="rangefield-name" defaultValue={50} label="Range field label">
{({ fieldProps }) => <Range {...fieldProps} min={0} max={100} />}
</RangeField>
<Field<ValueType<OptionType>>
label="Select field label"
name="select-field-name"
id="owner"
component={({ fieldProps }) => (
<Select
isSearchable={false}
inputId={fieldProps.id}
options={[
{ label: 'Option 1', value: 'option1' },
{ label: 'Option 2', value: 'option2' },
{ label: 'Option 3', value: 'option3' },
{ label: 'Option 4', value: 'option4' },
]}
{...fieldProps}
/>
)}
/>
<Fieldset legend="Radio group label">
<Field
name="color-selection"
component={({ fieldProps: { value, ...rest } }) => (
<RadioGroup
options={[
{ name: 'radio', value: 'option1', label: 'Option 1' },
{
name: 'radio',
value: 'option2',
label: 'Option 2',
},
{ name: 'radio', value: 'option3', label: 'Option 3' },
{ name: 'radio', value: 'option4', label: 'Option 4' },
]}
value={value}
{...rest}
/>
)}
/>
</Fieldset>
<CheckboxField name="toggle-default">
{({ fieldProps }) => (
<Flex alignItems="center">
<Box>
<Label htmlFor="toggle-default">Toggle label</Label>
</Box>
<Toggle {...fieldProps} id="toggle-default" value="test value" />
</Flex>
)}
</CheckboxField>
</FormSection>
<FormFooter align="start">
<ButtonGroup label="Form submit options">
<Button type="submit" appearance="primary" isLoading={submitting}>
Submit
</Button>
<Button appearance="subtle">Cancel</Button>
</ButtonGroup>
</FormFooter>
</form>
)}
</Form>
</Flex>
);
export default FormAllOptionsExample;Validation
Use validation messages to show when a form submission fails or requires more information. Keep messages short and for help writing them, use error messaging guidelines.
When validating text fields, styles will switch depending on whether it’s an error or warning message type. For example, helper text turns into an error message when someone's input doesn't fit the criteria. Error and warning messages disappear when the criteria is met.
To ensure error messages are rendered through assistive technologies, wrap them in MessageWrapper
and make sure they're in the DOM at the time the form is rendered.
Field-level validation
Validate a field's value using the validate prop. This accepts a function that receives the
current field value and is called whenever a field value changes.
Return an error when it is invalid. Otherwise, return undefined.
import React, { Fragment } from 'react';
import Button from '@atlaskit/button/default/button';
import Field from '@atlaskit/form/field';
import Form from '@atlaskit/form/form';
import { FormFooter } from '@atlaskit/form/form-footer';
import { FormHeader } from '@atlaskit/form/form-header';
import { ErrorMessage } from '@atlaskit/form/error-message';
import { MessageWrapper } from '@atlaskit/form/message-wrapper';
import { RequiredAsterisk } from '@atlaskit/form/required-asterisk';
import { Flex } from '@atlaskit/primitives/compiled/flex';
import { Text } from '@atlaskit/primitives/compiled/text';
import Select from '@atlaskit/select/default';
import { type ValueType } from '@atlaskit/select/types';
import TextField from '@atlaskit/textfield/text-field';
interface Option {
label: string;
value: string;
}
const members = [
{ label: 'Arni Singh', value: 'asingh' },
{ label: 'Hermione Walters', value: 'hwalters' },
{ label: 'Parvi Karan', value: 'pkaran' },
{ label: 'Charlie Li', value: 'cli' },
{ label: 'Silus Graham', value: 'sgraham' },
{ label: 'Jorge Oroza', value: 'joroza' },
];
const userNameData = ['jsmith', 'mchan'];
const errorMessages = {
shortUsername: 'Enter a team name longer than 4 characters.',
usernameInUse: 'This team name is already taken. Use a different name',
usernameIsRequired: 'A team name is required.',
selectError: 'Select at least one team member.',
};
const checkUserName = (value: string | undefined) => {
return value && userNameData.includes(value);
};
export default function FieldLevelValidationExample(): React.JSX.Element {
const handleSubmit = (formState: { command: string }) => {
console.log('form state', formState);
};
return (
<Flex direction="column">
<Form noValidate onSubmit={handleSubmit}>
<FormHeader title="Create team">
<Text as="p" aria-hidden={true}>
Required fields are marked with an asterisk <RequiredAsterisk />
</Text>
</FormHeader>
<Field
name="team"
label="Team name"
defaultValue=""
isRequired
validate={(value) => {
if (!value) {
return errorMessages.usernameIsRequired;
} else if (value.length <= 5) {
return errorMessages.shortUsername;
} else if (checkUserName(value)) {
return errorMessages.usernameInUse;
}
}}
component={({ fieldProps }) => <TextField {...fieldProps} />}
/>
<Field<ValueType<Option, true>>
name="members"
label="Team members"
defaultValue={[]}
isRequired
validate={(value) => {
if (!value || value.length === 0) {
return errorMessages.selectError;
}
}}
>
{({ fieldProps: { id, ...rest }, error }) => {
return (
<Fragment>
<Select<Option, true>
placeholder=""
inputId={id}
{...rest}
options={members}
isMulti
isClearable
clearControlLabel="Clear color"
descriptionId={error ? `${id}-error` : undefined}
/>
<MessageWrapper>{error && <ErrorMessage>{error}</ErrorMessage>}</MessageWrapper>
</Fragment>
);
}}
</Field>
<FormFooter align="start">
<Button type="submit" appearance="primary">
Create
</Button>
</FormFooter>
</Form>
</Flex>
);
}Submission validation
On submission, the current state gets passed onto the onSubmit handler.
For submission errors, the onSubmit handler should return an object. For example, if there's a
problem with the password field, the object should contain the key and the error as the value. If
the submission succeeds, the onSubmit handler should return undefined.
The onSubmit handler can return synchronously or return a promise that resolves with the error.
For successful asynchronous validation, the onSubmit handler would return a promise that resolves
to undefined as there are no errors.
import React from 'react';
import ButtonGroup from '@atlaskit/button/button-group';
import Button from '@atlaskit/button/default/button';
import Form from '@atlaskit/form/form';
import Field from '@atlaskit/form/field';
import { FormFooter } from '@atlaskit/form/form-footer';
import { FormHeader } from '@atlaskit/form/form-header';
import { RequiredAsterisk } from '@atlaskit/form/required-asterisk';
import { Flex } from '@atlaskit/primitives/compiled';
import RadioGroup from '@atlaskit/radio/radio-group';
import TextField from '@atlaskit/textfield/text-field';
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const createUser = async (data: { name: string; email: string }) => {
await sleep(500);
const errors = {
name: !data.name ? 'Enter a name' : undefined,
email: !data.email.includes('@')
? 'Enter a valid email address. For example: lpeters@atlassian.com'
: undefined,
};
if (!errors.name && !errors.email) {
console.log(data);
}
return errors;
};
const FormSubmissionValidationExample = (): React.JSX.Element => {
const handleSubmit = (data: { name: string; email: string }) => {
return createUser(data);
};
return (
<Flex direction="column">
<Form onSubmit={handleSubmit}>
{({ formProps }) => (
<form noValidate {...formProps}>
<FormHeader title="Add permissions">
<p aria-hidden="true">
Required fields are marked with an asterisk <RequiredAsterisk />
</p>
</FormHeader>
<Field
name="name"
label="Name"
defaultValue=""
isRequired
component={({ fieldProps }) => <TextField {...fieldProps} />}
/>
<Field
name="email"
label="Email"
defaultValue=""
isRequired
helperMessage="Must contain an @ symbol."
component={({ fieldProps }) => <TextField {...fieldProps} />}
/>
<Field
name="permissions"
label="Permissions"
component={({ fieldProps: { value, ...others } }) => (
<RadioGroup
options={[
{ name: 'permissions', value: 'view', label: 'View only' },
{
name: 'permissions',
value: 'edit',
label: 'Edit',
},
{ name: 'permissions', value: 'admin', label: 'Admin' },
]}
value={value}
{...others}
/>
)}
/>
<FormFooter align="start">
<ButtonGroup label="Form submit options">
<Button appearance="primary" id="create-repo-button" type="submit">
Add
</Button>
<Button appearance="subtle" id="create-repo-cancel">
Cancel
</Button>
</ButtonGroup>
</FormFooter>
</form>
)}
</Form>
</Flex>
);
};
export default FormSubmissionValidationExample;Asynchronous validation
If validation requires an async check, the validation function can return a promise. The promise should resolve with the error rather than reject with the error.
Field-level and submission validation can also have async validation where they return promises.
Using the validating status in the meta prop helps with asynchronous validation and provides a
better user experience. For example, a spinner reassures someone
that validation is happening.
import React, { Fragment } from 'react';
import ButtonGroup from '@atlaskit/button/button-group';
import Button from '@atlaskit/button/default/button';
import { Checkbox } from '@atlaskit/checkbox/checkbox';
import Form from '@atlaskit/form/form';
import { CheckboxField } from '@atlaskit/form/checkbox-field';
import { ErrorMessage } from '@atlaskit/form/error-message';
import Field from '@atlaskit/form/field';
import { FormFooter } from '@atlaskit/form/form-footer';
import { FormHeader } from '@atlaskit/form/form-header';
import { HelperMessage } from '@atlaskit/form/helper-message';
import { MessageWrapper } from '@atlaskit/form/message-wrapper';
import { RequiredAsterisk } from '@atlaskit/form/required-asterisk';
import { Flex, Text } from '@atlaskit/primitives/compiled';
import TextField from '@atlaskit/textfield/text-field';
export default (): React.JSX.Element => {
const simpleMemoize = <T, U>(fn: (arg: T) => U): ((arg: T) => U) => {
let lastArg: T;
let lastResult: U;
return (arg: T): U => {
if (arg !== lastArg) {
lastArg = arg;
lastResult = fn(arg);
}
return lastResult;
};
};
const validateName = (value: string = '') => {
if (!value) {
return 'A name is required.';
}
if (value.length < 6) {
return 'The name must be longer than 5 characters.';
}
return undefined;
};
const validateDescription = simpleMemoize((value: string = '') => {
if (!value) {
return 'A description is required.';
}
if (value.length < 8) {
return new Promise((resolve) => setTimeout(resolve, 300)).then(
() => 'The description must be longer than 7 characters.',
);
}
return undefined;
});
return (
<Flex direction="column">
<Form<{ name: string; description: string; remember: boolean }>
noValidate
onSubmit={(data) => {
console.log('form data', data);
return new Promise((resolve) => setTimeout(resolve, 2000)).then(() =>
data.name === 'error' ? { name: 'This name has been used. Try again.' } : undefined,
);
}}
>
{({ formProps, submitting }) => (
<form {...formProps}>
<FormHeader title="Add work type">
<Text as="p" aria-hidden="true">
Required fields are marked with an asterisk <RequiredAsterisk />
</Text>
</FormHeader>
<Field
name="name"
label="Name"
isRequired
defaultValue=""
helperMessage="Must be 5 or more characters."
validate={validateName}
component={({ fieldProps }) => <TextField autoComplete="name" {...fieldProps} />}
/>
<Field
name="description"
label="Description"
defaultValue=""
isRequired
validate={validateDescription}
>
{({ fieldProps, error, meta }) => (
<Fragment>
<TextField type="description" {...fieldProps} />
<MessageWrapper>
{error && <ErrorMessage>{error}</ErrorMessage>}
{meta.validating && meta.dirty ? (
<HelperMessage>Checking...</HelperMessage>
) : null}
</MessageWrapper>
</Fragment>
)}
</Field>
<CheckboxField name="remember">
{({ fieldProps }) => <Checkbox {...fieldProps} label="Add another work item" />}
</CheckboxField>
<FormFooter>
<ButtonGroup label="Form submit options">
<Button appearance="subtle">Cancel</Button>
<Button type="submit" appearance="primary" isLoading={submitting}>
Add
</Button>
</ButtonGroup>
</FormFooter>
</form>
)}
</Form>
</Flex>
);
};Types of forms
Forms in modal dialogs
To display a form in a layer above the page, use it within a modal dialog component.
Keep the context of the form inside the modal, as people can't access anything outside of it while it's being filled in.
Buttons to submit the form should be aligned to the right, with the primary button to the right of a secondary button.
import React, { useState } from 'react';
import Button from '@atlaskit/button/default/button';
import Form from '@atlaskit/form/form';
import Field from '@atlaskit/form/field';
import ModalDialog from '@atlaskit/modal-dialog/modal-dialog';
import ModalBody from '@atlaskit/modal-dialog/modal-body';
import ModalFooter from '@atlaskit/modal-dialog/modal-footer';
import ModalHeader from '@atlaskit/modal-dialog/modal-header';
import ModalTitle from '@atlaskit/modal-dialog/modal-title';
import ModalTransition from '@atlaskit/modal-dialog/modal-transition';
import RadioGroup from '@atlaskit/radio/radio-group';
import Textfield from '@atlaskit/textfield/text-field';
const FormModalDialogExample = (): React.JSX.Element => {
const [isOpen, setIsOpen] = useState(false);
const open = () => setIsOpen(true);
const close = () => setIsOpen(false);
return (
<>
<Button onClick={open}>Open modal</Button>
<ModalTransition>
{isOpen && (
<ModalDialog onClose={close}>
<Form
onSubmit={(value) =>
window.alert(`You submitted:\n${JSON.stringify(value, undefined, 2)}`)
}
id="form-with-id"
>
<ModalHeader hasCloseButton>
<ModalTitle>Add permissions</ModalTitle>
</ModalHeader>
<ModalBody>
<Field
label="Name"
name="my-name"
defaultValue=""
component={({ fieldProps }) => <Textfield {...fieldProps} />}
/>
<Field
label="Email"
name="my-email"
defaultValue=""
component={({ fieldProps }) => <Textfield autoComplete="off" {...fieldProps} />}
/>
<Field
name="permission"
label="Permissions"
defaultValue=""
component={({ fieldProps: { value, ...others } }) => (
<RadioGroup
options={[
{ name: 'permission', value: 'view-only', label: 'View only' },
{ name: 'permission', value: 'edit', label: 'Edit access' },
{ name: 'permission', value: 'admin', label: 'Admin' },
]}
{...others}
/>
)}
/>
</ModalBody>
<ModalFooter>
<Button onClick={close} appearance="subtle">
Cancel
</Button>
<Button type="submit" form="form-with-id" appearance="primary">
Add
</Button>
</ModalFooter>
</Form>
</ModalDialog>
)}
</ModalTransition>
</>
);
};
export default FormModalDialogExample;Single page forms
Use when you have a small amount of information to capture.
Buttons to submit the form should be aligned to the left, with the primary button to the left of a secondary button. This alignment aids scanning and helps people using screen magnifiers and who have a limited field of vision.
Listening to form state
Form previews
To use the current form state to generate a preview while someone is filling a form in, use the
useFormState hook.
Don't use these values for any permanent storage or state. You should rely on form submission for the final values.
import Banner from '@atlaskit/banner';
import { cssMap, jsx } from '@atlaskit/css';
import Form from '@atlaskit/form/form';
import Field from '@atlaskit/form/field';
import { useFormState } from '@atlaskit/form/use-form-state';
import Select from '@atlaskit/select/default';
import type { ValueType as Value } from '@atlaskit/select/types';
import TextArea from '@atlaskit/textarea/text-area';
import { token } from '@atlaskit/tokens';
interface Option {
label: string;
value: 'warning' | 'error' | 'announcement';
}
const styles = cssMap({
formContainer: {
maxWidth: '400px',
margin: '0 auto',
},
preview: {
marginBlockStart: token('space.200'),
},
});
type BannerForm = {
appearance: Option;
content: string;
};
const FormPreview = () => {
const formState = useFormState<BannerForm>({
values: true,
pristine: true,
dirty: true,
});
return (
<div css={styles.preview}>
<Banner appearance={formState?.values.appearance.value}>{formState?.values.content}</Banner>
<pre>{JSON.stringify(formState, null, 2)}</pre>;
</div>
);
};
export default function StateSubscriptionExample(): JSX.Element {
return (
<Form
onSubmit={(data) => {
console.log('form data', data);
}}
>
<div css={styles.formContainer}>
<Field<string, HTMLTextAreaElement>
name="content"
defaultValue=" "
label="Banner content"
component={({ fieldProps }) => <TextArea {...fieldProps} />}
/>
<Field<Value<Option>>
name="appearance"
label="Banner appearance"
defaultValue={{ label: 'Announcement', value: 'announcement' }}
component={({ fieldProps: { id, ...rest } }) => (
<Select<Option>
inputId={id}
{...rest}
options={[
{ label: 'Announcement', value: 'announcement' },
{ label: 'Error', value: 'error' },
{ label: 'Warning', value: 'warning' },
]}
isClearable
clearControlLabel="Clear appearance"
/>
)}
/>
</div>
<FormPreview />
</Form>
);
}Conditional fields
Include conditional fields in your form by checking the form state with useFormState.
This is particularly useful for building forms that have progressive disclosure.
import React from 'react';
import Form from '@atlaskit/form/form';
import Field from '@atlaskit/form/field';
import { useFormState } from '@atlaskit/form/use-form-state';
import RadioGroup from '@atlaskit/radio/radio-group';
import TextField from '@atlaskit/textfield/text-field';
const LoginForm = () => (
<>
<Field
name="email"
label="Email"
defaultValue=""
isRequired
component={({ fieldProps }) => <TextField {...fieldProps} />}
/>
<Field
name="password"
label="Password"
defaultValue=""
isRequired
component={({ fieldProps }) => <TextField {...fieldProps} />}
/>
</>
);
const SignUpForm = () => (
<>
<Field
name="name"
label="Name"
defaultValue=""
isRequired
component={({ fieldProps }) => <TextField {...fieldProps} />}
/>
<Field
name="email"
label="Email"
defaultValue=""
isRequired
component={({ fieldProps }) => <TextField {...fieldProps} />}
/>
<Field
name="password"
label="Password"
defaultValue=""
isRequired
component={({ fieldProps }) => <TextField {...fieldProps} />}
/>
<Field
name="confirmPassword"
label="Confirm password"
defaultValue=""
isRequired
component={({ fieldProps }) => <TextField {...fieldProps} />}
/>
</>
);
function ConditionalFieldsExample(): React.JSX.Element {
const formState = useFormState({ values: true });
return (
<>
<Field
label="Do you have an existing account?"
name="existingAccount"
defaultValue=""
isRequired
component={({ fieldProps }) => (
<RadioGroup
{...fieldProps}
options={[
{ name: 'existingAccount', value: 'yes', label: 'Yes' },
{ name: 'existingAccount', value: 'no', label: 'No' },
]}
/>
)}
/>
{formState?.values.existingAccount === 'yes' ? <LoginForm /> : <SignUpForm />}
</>
);
}
export default (): React.JSX.Element => {
return (
<Form onSubmit={(data) => console.log('form data', data)}>
<ConditionalFieldsExample />
</Form>
);
};