component
Input
A 56px text field with a floating label, an optional lock icon, a line of help text and an error state.
Supportive
Supportive
Supportive
Supportive
Supportive
Supportive
Supportive
Supportive
Supportive
Supportive
Hover, press or tab to one to see the other 4 states.
import { Input } from 'rost/components/input/Input'
<Input label="Email address" type="email" supportive="We'll send the receipt here." />
<Input label="Amount" error supportive="Use a number, like 1200." />src/components/input/Input.tsx
import { useId, useRef, useState, type InputHTMLAttributes, type ReactNode } from 'react'
import { IconButton } from '../icon-button/IconButton'
import './input.css'
export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
/** The field's name. It sits where you type until there's a value or focus, then moves up. */
readonly label: ReactNode
/** One line of help under the field, or the fix when there's an error. */
readonly supportive?: ReactNode
readonly error?: boolean
/** The leading icon. Figma's lock by default; `null` for none. */
readonly icon?: ReactNode
/** Forces the focused look, for previews and tests. In use, it follows focus. */
readonly selected?: boolean
/** Called by the clear button. Without it there's no clear button. */
readonly onClear?: () => void
}
const b = 'rost-input'
/**
* A 56px text field with a floating label. Empty, it shows the label where
* you'll type; with a value or focus, the label moves up small. Built from
* contracts/input.contract.yaml.
*/
export function Input({
label, supportive, error = false, icon, selected, onClear, className, id, value, defaultValue,
onChange, onFocus, onBlur, disabled, ...rest
}: InputProps) {
const own = useId()
const inputId = id ?? own
const helpId = `${inputId}-help`
const [typed, setTyped] = useState(String(defaultValue ?? ''))
const [focused, setFocused] = useState(false)
const ref = useRef<HTMLInputElement>(null)
const text = value !== undefined ? String(value) : typed
const filled = text.length > 0
const isSelected = selected ?? focused
const cls = [b, `${b}--configuration-${filled ? 'filled' : 'empty'}`, isSelected && `${b}--selected`, error && `${b}--error`, className].filter(Boolean).join(' ')
return (
// The whole field is the target: a press anywhere in it, other than on the
// clear button, puts the caret in the input. Empty, the input is 0px tall.
<div className={cls} onMouseDown={(e) => {
if (disabled || e.target === ref.current || (e.target as Element).closest('button')) return
e.preventDefault(); ref.current?.focus()
}}>
<span className={`${b}__outline`} />
<span className={`${b}__left`}>
{icon === undefined ? <span className={`${b}__icons-lock`} aria-hidden="true" /> : icon}
<span className={`${b}__content`}>
<label className={`${b}__label`} htmlFor={inputId}>{label}</label>
<input
ref={ref}
id={inputId}
className={`${b}__value`}
value={value}
defaultValue={value === undefined ? defaultValue : undefined}
disabled={disabled}
aria-invalid={error || undefined}
aria-describedby={supportive ? helpId : undefined}
onChange={(e) => { if (value === undefined) setTyped(e.target.value); onChange?.(e) }}
onFocus={(e) => { setFocused(true); onFocus?.(e) }}
onBlur={(e) => { setFocused(false); onBlur?.(e) }}
{...rest}
/>
</span>
</span>
{error ? (
<span className={`${b}__right`}><span className={`${b}__icons-alarm`} aria-hidden="true" /></span>
) : filled && onClear && !disabled ? (
<span className={`${b}__right`}>
<IconButton variant="project" aria-label="Clear" icon={<span className={`${b}__clear-glyph`} />} onClick={() => { onClear(); ref.current?.focus() }} />
</span>
) : null}
{supportive && <span className={`${b}__supportive`} id={helpId}>{supportive}</span>}
<span className={`${b}__focus-ring`} />
</div>
)
}When to use
- Any single line of text: email, name, an amount, search.
When not to use
- Picking from a known list. That's Chip or Toggle, free text just invites typos.
- Several lines of text. There's no textarea in the kit yet.
Behaviour
Four things change it:
configuration:Emptyshows the label where you'll type.Filledmoves it up and shows the value.selected: it has focus, so the focus ring shows.error: the outline, label, help text and icons go red, and the alarm icon shows up on the right.state:Default,HoverandDisabled.
Check what people typed when they leave the field or submit, not on every key.
Accessibility
- Element: a real
<input>with a<label for>. The floating label is the label. Never aplaceholder. - Supportive Text: connected with
aria-describedby. - Error:
aria-invalid="true", and the help text says how to fix it. - Colour: an error shows as the alarm icon and the message, not only red.
- Focus: the ring shows on
:focus-visibleand when you arrive by keyboard. Never remove the outline without the ring. - Lock Icon: just decoration, unless it means read-only. If it does, set
readonlyand say so in the help text.
Content
Label: a noun, sentence case ("Email address"). Help text: one line. Before an error it's a hint, after it it's the fix ("Use at least 8 characters", not "Invalid input").
Props
| Prop | Type | Values | Default |
|---|---|---|---|
| label | slot | ||
| value | slot | ||
| placeholder | slot | ||
| supportive | slot | ||
| configuration | enum | Empty · Filled | Empty |
| selected | boolean | false | |
| error | boolean | false | |
| icon | slot |
States: Default, Hover, Disabled.
Where it's used
- runceanu.com
src/rost/input/Input.tsx.rost-input - akount.ing
src/components/TextField.tsx.ak-field__box
Values
Straight from the contract. Each value sits next to its token, because six tokens here all come out as #3a3a3a and the colour alone can't tell you which one is right.
Never changes
| Part | Property | Value | Token |
|---|---|---|---|
| root | display | flex | layout |
| root | align-items | center | layout |
| root | position | relative | layout |
| root | gap | 8px | spacing/gap/small |
| root | height | 56px | spacing/size/large |
| root | padding-left | 12px | spacing/gap/small-medium |
| root | padding-right | 12px | spacing/gap/small-medium |
| root | padding-top | 0px | spacing/gap/none |
| root | padding-bottom | 0px | spacing/gap/none |
| root | border-radius | 12px | spacing/radius/medium |
| root | cursor | text | Figma frame size |
| root | width | 208px | Figma frame size |
| outline | position | absolute | layout |
| outline | inset | 0 | layout |
| outline | border-style | solid | no token yet |
| outline | border-width | 1px | spacing/outline/small |
| outline | border-radius | 12px | spacing/radius/medium |
| outline | opacity | 1 | opacity/solid |
| outline | pointer-events | none | Figma frame size |
| left | display | flex | layout |
| left | align-items | center | layout |
| left | flex | 1 0 0 | layout |
| left | min-width | 1px | Figma frame size |
| left | position | relative | layout |
| left | gap | 8px | spacing/gap/small |
| icons-lock | display | block | layout |
| icons-lock | flex-shrink | 0 | layout |
| icons-lock | width | 20px | spacing/size/xsmall |
| icons-lock | height | 20px | spacing/size/xsmall |
| icons-lock | mask | url(./icon-lock.svg) center / contain no-repeat | on purpose: The icon file itself. |
| content | display | flex | layout |
| content | flex-direction | column | layout |
| content | align-items | flex-start | layout |
| content | flex | 1 0 0 | layout |
| content | min-width | 1px | Figma frame size |
| content | position | relative | layout |
| content | white-space | nowrap | layout |
| label | font-family | 'General Sans Variable' | font-family/base |
| label | font-weight | 400 | typography/label/font-weight/regular |
| label | pointer-events | none | Figma frame size |
| value | display | block | layout |
| value | width | 100% | layout |
| value | padding | 0px | spacing/gap/none |
| value | border | none | Figma frame size |
| value | background | none | Figma frame size |
| value | outline | none | Figma frame size |
| value | font-family | 'General Sans Variable' | font-family/base |
| value | font-weight | 400 | typography/paragraph/font-weight/regular |
| value | font-size | 14px | typography/paragraph/font-size |
| value | line-height | 20px | typography/paragraph/line-height |
| value | letter-spacing | 0px | typography/paragraph/letter-spacing |
| right | display | flex | layout |
| right | align-items | center | layout |
| right | flex-shrink | 0 | layout |
| right | position | relative | layout |
| clear-glyph | display | block | layout |
| clear-glyph | width | 16px | spacing/size/2xsmall |
| clear-glyph | height | 16px | spacing/size/2xsmall |
| clear-glyph | background | currentColor | no token yet |
| clear-glyph | mask | url(./icon-clear.svg) center / contain no-repeat | on purpose: The icon file itself. |
| icons-alarm | display | block | layout |
| icons-alarm | flex-shrink | 0 | layout |
| icons-alarm | width | 24px | spacing/size/small |
| icons-alarm | height | 24px | spacing/size/small |
| icons-alarm | mask | url(./icon-alarm.svg) center / contain no-repeat | on purpose: The icon file itself. |
| supportive | position | absolute | layout |
| supportive | left | 12px | spacing/gap/small-medium |
| supportive | top | calc(var(--spacing-size-large) + var(--spacing-gap-2xsmall)) | on purpose: Composed from tokens: the field's height plus spacing/gap/2xsmall. |
| supportive | white-space | nowrap | layout |
| supportive | font-family | 'General Sans Variable' | font-family/base |
| supportive | font-weight | 500 | typography/label/font-weight/medium |
| supportive | font-size | 10px | typography/placeholder/font-size |
| supportive | line-height | 12px | typography/placeholder/line-height |
| supportive | letter-spacing | 0px | typography/placeholder/letter-spacing |
| focus-ring | display | none | layout |
| focus-ring | position | absolute | layout |
| focus-ring | inset | calc(-1 * var(--spacing-gap-2xsmall)) | layout |
| focus-ring | border-style | solid | no token yet |
| focus-ring | border-width | 2px | spacing/outline/medium |
| focus-ring | border-radius | 16px | spacing/radius/large |
| focus-ring | pointer-events | none | Figma frame size |
Changes by variant
focus-ring.border-color4
| state=Default, configuration=Empty, selected=True, error=False | #00d669 | input/regular/focus/focus-ring |
| state=Default, configuration=Empty, selected=True, error=True | #00d669 | input/negative/focus/focus-ring |
| state=Default, configuration=Filled, selected=True, error=True | #00d669 | input/negative/focus/focus-ring |
| state=Default, configuration=Filled, selected=True, error=False | #00d669 | input/regular/focus/focus-ring |
focus-ring.display4
| state=Default, configuration=Empty, selected=True, error=False | block | layout |
| state=Default, configuration=Empty, selected=True, error=True | block | layout |
| state=Default, configuration=Filled, selected=True, error=True | block | layout |
| state=Default, configuration=Filled, selected=True, error=False | block | layout |
icons-alarm.background6
| state=Default, configuration=Empty, selected=False, error=True | #e93120 | input/negative/default/icon |
| state=Default, configuration=Filled, selected=False, error=True | #e93120 | input/negative/default/icon |
| state=Default, configuration=Empty, selected=True, error=True | #e93120 | input/negative/focus/icon |
| state=Default, configuration=Filled, selected=True, error=True | #e93120 | input/negative/focus/icon |
| state=Hover, configuration=Empty, selected=False, error=True | #ae1b13 | input/negative/hover/icon |
| state=Hover, configuration=Filled, selected=False, error=True | #ae1b13 | input/negative/hover/icon |
icons-lock.background14
| state=Default, configuration=Empty, selected=False, error=False | #00d669 | input/regular/default/icon |
| state=Hover, configuration=Empty, selected=False, error=False | #00b450 | input/regular/hover/icon |
| state=Default, configuration=Empty, selected=True, error=False | #00d669 | input/regular/focus/icon |
| state=Disabled, configuration=Empty, selected=False, error=False | #b6ffda | input/regular/disabled/icon |
| state=Default, configuration=Empty, selected=False, error=True | #e93120 | input/negative/default/icon |
| state=Default, configuration=Filled, selected=False, error=True | #e93120 | input/negative/default/icon |
| state=Default, configuration=Empty, selected=True, error=True | #e93120 | input/negative/focus/icon |
| state=Default, configuration=Filled, selected=True, error=True | #e93120 | input/negative/focus/icon |
| state=Hover, configuration=Empty, selected=False, error=True | #ae1b13 | input/negative/hover/icon |
| state=Hover, configuration=Filled, selected=False, error=True | #ae1b13 | input/negative/hover/icon |
| state=Default, configuration=Filled, selected=False, error=False | #00d669 | input/regular/default/icon |
| state=Disabled, configuration=Filled, selected=False, error=False | #b6ffda | input/regular/disabled/icon |
| state=Default, configuration=Filled, selected=True, error=False | #00d669 | input/regular/focus/icon |
| state=Hover, configuration=Filled, selected=False, error=False | #00b450 | input/regular/hover/icon |
icons-lock.opacity2
| state=Disabled, configuration=Empty, selected=False, error=False | 0.16 | opacity/soft |
| state=Disabled, configuration=Filled, selected=False, error=False | 0.16 | opacity/soft |
label.color14
| state=Default, configuration=Empty, selected=False, error=False | #999999 | input/regular/default/label |
| state=Hover, configuration=Empty, selected=False, error=False | #d3d9d5 | input/regular/hover/supportive |
| state=Default, configuration=Empty, selected=True, error=False | #999999 | input/regular/focus/label |
| state=Disabled, configuration=Empty, selected=False, error=False | #555555 | input/regular/disabled/label |
| state=Default, configuration=Empty, selected=False, error=True | #e93120 | input/negative/default/label |
| state=Default, configuration=Filled, selected=False, error=True | #e93120 | input/negative/default/label |
| state=Default, configuration=Empty, selected=True, error=True | #e93120 | input/negative/focus/label |
| state=Default, configuration=Filled, selected=True, error=True | #e93120 | input/negative/focus/label |
| state=Hover, configuration=Empty, selected=False, error=True | #e93120 | input/negative/hover/label |
| state=Hover, configuration=Filled, selected=False, error=True | #e93120 | input/negative/hover/label |
| state=Default, configuration=Filled, selected=False, error=False | #999999 | input/regular/default/label |
| state=Disabled, configuration=Filled, selected=False, error=False | #555555 | input/regular/disabled/label |
| state=Default, configuration=Filled, selected=True, error=False | #999999 | input/regular/focus/label |
| state=Hover, configuration=Filled, selected=False, error=False | #d3d9d5 | input/regular/hover/label |
label.font-size14
| state=Default, configuration=Empty, selected=False, error=False | 14px | typography/paragraph/font-size |
| state=Hover, configuration=Empty, selected=False, error=False | 14px | typography/paragraph/font-size |
| state=Default, configuration=Empty, selected=True, error=False | 10px | typography/placeholder/font-size |
| state=Disabled, configuration=Empty, selected=False, error=False | 14px | typography/paragraph/font-size |
| state=Default, configuration=Empty, selected=False, error=True | 14px | typography/paragraph/font-size |
| state=Default, configuration=Filled, selected=False, error=True | 10px | typography/placeholder/font-size |
| state=Default, configuration=Empty, selected=True, error=True | 10px | typography/placeholder/font-size |
| state=Default, configuration=Filled, selected=True, error=True | 10px | typography/placeholder/font-size |
| state=Hover, configuration=Empty, selected=False, error=True | 14px | typography/paragraph/font-size |
| state=Hover, configuration=Filled, selected=False, error=True | 10px | typography/placeholder/font-size |
| state=Default, configuration=Filled, selected=False, error=False | 10px | typography/placeholder/font-size |
| state=Disabled, configuration=Filled, selected=False, error=False | 10px | typography/placeholder/font-size |
| state=Default, configuration=Filled, selected=True, error=False | 10px | typography/placeholder/font-size |
| state=Hover, configuration=Filled, selected=False, error=False | 10px | typography/placeholder/font-size |
label.letter-spacing14
| state=Default, configuration=Empty, selected=False, error=False | 0px | typography/paragraph/letter-spacing |
| state=Hover, configuration=Empty, selected=False, error=False | 0px | typography/paragraph/letter-spacing |
| state=Default, configuration=Empty, selected=True, error=False | 0px | typography/placeholder/letter-spacing |
| state=Disabled, configuration=Empty, selected=False, error=False | 0px | typography/paragraph/letter-spacing |
| state=Default, configuration=Empty, selected=False, error=True | 0px | typography/paragraph/letter-spacing |
| state=Default, configuration=Filled, selected=False, error=True | 0px | typography/placeholder/letter-spacing |
| state=Default, configuration=Empty, selected=True, error=True | 0px | typography/placeholder/letter-spacing |
| state=Default, configuration=Filled, selected=True, error=True | 0px | typography/placeholder/letter-spacing |
| state=Hover, configuration=Empty, selected=False, error=True | 0px | typography/paragraph/letter-spacing |
| state=Hover, configuration=Filled, selected=False, error=True | 0px | typography/placeholder/letter-spacing |
| state=Default, configuration=Filled, selected=False, error=False | 0px | typography/placeholder/letter-spacing |
| state=Disabled, configuration=Filled, selected=False, error=False | 0px | typography/placeholder/letter-spacing |
| state=Default, configuration=Filled, selected=True, error=False | 0px | typography/placeholder/letter-spacing |
| state=Hover, configuration=Filled, selected=False, error=False | 0px | typography/placeholder/letter-spacing |
label.line-height14
| state=Default, configuration=Empty, selected=False, error=False | 20px | typography/paragraph/line-height |
| state=Hover, configuration=Empty, selected=False, error=False | 20px | typography/paragraph/line-height |
| state=Default, configuration=Empty, selected=True, error=False | 12px | typography/placeholder/line-height |
| state=Disabled, configuration=Empty, selected=False, error=False | 20px | typography/paragraph/line-height |
| state=Default, configuration=Empty, selected=False, error=True | 20px | typography/paragraph/line-height |
| state=Default, configuration=Filled, selected=False, error=True | 12px | typography/placeholder/line-height |
| state=Default, configuration=Empty, selected=True, error=True | 12px | typography/placeholder/line-height |
| state=Default, configuration=Filled, selected=True, error=True | 12px | typography/placeholder/line-height |
| state=Hover, configuration=Empty, selected=False, error=True | 20px | typography/paragraph/line-height |
| state=Hover, configuration=Filled, selected=False, error=True | 12px | typography/placeholder/line-height |
| state=Default, configuration=Filled, selected=False, error=False | 12px | typography/placeholder/line-height |
| state=Disabled, configuration=Filled, selected=False, error=False | 12px | typography/placeholder/line-height |
| state=Default, configuration=Filled, selected=True, error=False | 12px | typography/placeholder/line-height |
| state=Hover, configuration=Filled, selected=False, error=False | 12px | typography/placeholder/line-height |
outline.background14
| state=Default, configuration=Empty, selected=False, error=False | #060805 | input/regular/default/background |
| state=Hover, configuration=Empty, selected=False, error=False | #171717 | input/regular/hover/background |
| state=Default, configuration=Empty, selected=True, error=False | #060805 | input/regular/focus/background |
| state=Disabled, configuration=Empty, selected=False, error=False | #171717 | input/regular/disabled/background |
| state=Default, configuration=Empty, selected=False, error=True | #060805 | input/negative/default/background |
| state=Default, configuration=Filled, selected=False, error=True | #060805 | input/negative/default/background |
| state=Default, configuration=Empty, selected=True, error=True | #060805 | input/negative/focus/background |
| state=Default, configuration=Filled, selected=True, error=True | #060805 | input/negative/focus/background |
| state=Hover, configuration=Empty, selected=False, error=True | #171717 | input/negative/hover/background |
| state=Hover, configuration=Filled, selected=False, error=True | #171717 | input/negative/hover/background |
| state=Default, configuration=Filled, selected=False, error=False | #060805 | input/regular/default/background |
| state=Disabled, configuration=Filled, selected=False, error=False | #171717 | input/regular/disabled/background |
| state=Default, configuration=Filled, selected=True, error=False | #060805 | input/regular/focus/background |
| state=Hover, configuration=Filled, selected=False, error=False | #171717 | input/regular/hover/background |
outline.border-color14
| state=Default, configuration=Empty, selected=False, error=False | #555555 | input/regular/default/outline |
| state=Hover, configuration=Empty, selected=False, error=False | #999999 | input/regular/hover/outline |
| state=Default, configuration=Empty, selected=True, error=False | #555555 | input/regular/focus/outline |
| state=Disabled, configuration=Empty, selected=False, error=False | #3a3a3a | input/regular/disabled/outline |
| state=Default, configuration=Empty, selected=False, error=True | #f2705f | input/negative/default/outline |
| state=Default, configuration=Filled, selected=False, error=True | #f2705f | input/negative/default/outline |
| state=Default, configuration=Empty, selected=True, error=True | #f2705f | input/negative/focus/outline |
| state=Default, configuration=Filled, selected=True, error=True | #f2705f | input/negative/focus/outline |
| state=Hover, configuration=Empty, selected=False, error=True | #e93120 | input/negative/hover/outline |
| state=Hover, configuration=Filled, selected=False, error=True | #e93120 | input/negative/hover/outline |
| state=Default, configuration=Filled, selected=False, error=False | #555555 | input/regular/default/outline |
| state=Disabled, configuration=Filled, selected=False, error=False | #3a3a3a | input/regular/disabled/outline |
| state=Default, configuration=Filled, selected=True, error=False | #555555 | input/regular/focus/outline |
| state=Hover, configuration=Filled, selected=False, error=False | #999999 | input/regular/hover/outline |
supportive.color14
| state=Default, configuration=Empty, selected=False, error=False | #999999 | input/regular/default/supportive |
| state=Hover, configuration=Empty, selected=False, error=False | #d3d9d5 | input/regular/hover/supportive |
| state=Default, configuration=Empty, selected=True, error=False | #999999 | input/regular/focus/supportive |
| state=Disabled, configuration=Empty, selected=False, error=False | #555555 | input/regular/disabled/supportive |
| state=Default, configuration=Empty, selected=False, error=True | #e93120 | input/negative/default/supportive |
| state=Default, configuration=Filled, selected=False, error=True | #e93120 | input/negative/default/supportive |
| state=Default, configuration=Empty, selected=True, error=True | #e93120 | input/negative/focus/supportive |
| state=Default, configuration=Filled, selected=True, error=True | #e93120 | input/negative/focus/supportive |
| state=Hover, configuration=Empty, selected=False, error=True | #ae1b13 | input/negative/hover/supportive |
| state=Hover, configuration=Filled, selected=False, error=True | #ae1b13 | input/negative/hover/supportive |
| state=Default, configuration=Filled, selected=False, error=False | #999999 | input/regular/default/supportive |
| state=Disabled, configuration=Filled, selected=False, error=False | #555555 | input/regular/disabled/supportive |
| state=Default, configuration=Filled, selected=True, error=False | #999999 | input/regular/focus/supportive |
| state=Hover, configuration=Filled, selected=False, error=False | #d3d9d5 | input/regular/hover/supportive |
value.color9
| state=Default, configuration=Empty, selected=True, error=False | #c2c7c3 | input/regular/focus/placeholder |
| state=Default, configuration=Filled, selected=False, error=True | #999999 | input/negative/default/placeholder |
| state=Default, configuration=Empty, selected=True, error=True | #ededed | input/negative/focus/value |
| state=Default, configuration=Filled, selected=True, error=True | #ededed | input/negative/focus/value |
| state=Hover, configuration=Filled, selected=False, error=True | #555555 | input/negative/hover/placeholder |
| state=Default, configuration=Filled, selected=False, error=False | #c2c7c3 | input/regular/default/placeholder |
| state=Disabled, configuration=Filled, selected=False, error=False | #555555 | input/regular/disabled/placeholder |
| state=Default, configuration=Filled, selected=True, error=False | #c2c7c3 | input/regular/focus/placeholder |
| state=Hover, configuration=Filled, selected=False, error=False | #d3d9d5 | input/regular/hover/placeholder |
value.height5
| state=Default, configuration=Empty, selected=False, error=False | 0 | Figma frame size |
| state=Hover, configuration=Empty, selected=False, error=False | 0 | Figma frame size |
| state=Disabled, configuration=Empty, selected=False, error=False | 0 | Figma frame size |
| state=Default, configuration=Empty, selected=False, error=True | 0 | Figma frame size |
| state=Hover, configuration=Empty, selected=False, error=True | 0 | Figma frame size |
value.opacity5
| state=Default, configuration=Empty, selected=False, error=False | 0 | Figma frame size |
| state=Hover, configuration=Empty, selected=False, error=False | 0 | Figma frame size |
| state=Disabled, configuration=Empty, selected=False, error=False | 0 | Figma frame size |
| state=Default, configuration=Empty, selected=False, error=True | 0 | Figma frame size |
| state=Hover, configuration=Empty, selected=False, error=True | 0 | Figma frame size |