Getting Started
Foundations
Components
Search for a command to run...
A comprehensive understanding of states and state management creates the foundation for consistent, predictable, and maintainable user interfaces. This guide explains these concepts and provides practical applications for implementing them in our design system.
States represent the different conditions a component can exist in at any given moment. They determine how a component appears and behaves in response to user interactions, system events, or data changes.
All interactive components in our design system should account for these fundamental states:
| State | Description |
|---|---|
| Default | The initial appearance of a component before any interaction. This state establishes the component's baseline visual identity while indicating its purpose and functionality. |
| Hover | Appears when a cursor moves over an interactive element, providing a visual cue that the element can be interacted with. |
| Active | Occurs when a button or control is being interacted with (pressed, clicked, tapped). This brief state provides immediate feedback that the system has recognized the user's action. |
| Focus | Activated by keyboard navigation or programmatic focus. Focus states must include a 2px focus indicator with a 3:1 contrast ratio to ensure accessibility for keyboard users and those using assistive technologies. |
| Disabled | Indicates when an element exists but is unavailable for interaction. Disabled states should maintain the component's visual structure while clearly communicating its unavailability. |
| Error | Used to highlight invalid inputs or system errors. Error states must include both visual indicators and clear messaging with at least a 4:1 contrast ratio to meet accessibility standards. |
Depending on the component's complexity, these additional states may apply:
| State | Description |
|---|---|
| Loading | Indicates when a component is retrieving data or processing information. Loading states provide essential feedback that prevents user confusion during wait times. |
| Expanded/Collapsed | Used for components that can reveal or hide content, such as accordions, dropdowns, or expandable panels. |
| Selected | Indicates when an item within a collection has been chosen, such as items in a list, tabs, or menu options. |
| Indeterminate | Represents a state between checked and unchecked, primarily used in checkboxes when some (but not all) nested options are selected. |
Statefulness describes how components remember and maintain information about their condition between interactions.
Stateful components maintain memory of past interactions, store data that changes over time, and manage their own internal state. Examples include forms, toggles, accordions, and carousels.
These components require:
Stateless components don't maintain internal memory, rendering based solely on their input properties. The same input will always produce the same output. Examples include buttons, labels, icons, and dividers.
These components benefit from:
State management refers to the approaches used to control, organize, and maintain state in components and applications.
State that only affects a single component should be managed within that component. This approach maintains encapsulation and simplifies reasoning about the component's behavior.
// Example of component-level state
function Counter() {
const [count, setCount] = useState(0)
return (
<div className="counter">
<span>Count: {count}</span>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
)
}When multiple components need access to the same state, consider lifting state to a common parent or using a shared state mechanism. This creates a single source of truth and prevents synchronization issues.
For state that affects large portions of the application (such as authentication, themes, or global settings), use centralized state management. This approach provides consistency across the application and simplifies complex state interactions.
Controlled components receive their state from parent components and notify those parents of any requested changes:
// Controlled component example
function ControlledInput({ value, onChange }) {
return <input value={value} onChange={(e) => onChange(e.target.value)} />
}Uncontrolled components manage their state internally, often using refs to access values:
// Uncontrolled component example
function UncontrolledInput() {
const inputRef = useRef()
function handleSubmit() {
const value = inputRef.current.value
// Use the value...
}
return (
<form onSubmit={handleSubmit}>
<input ref={inputRef} defaultValue="" />
<button type="submit">Submit</button>
</form>
)
}For components with complex state logic, state machines provide a formal way to define states and transitions. They help prevent invalid states and make behavior more predictable.
// Simplified state machine pattern
const machine = {
initial: "idle",
states: {
idle: {
on: { SUBMIT: "loading" },
},
loading: {
on: { SUCCESS: "success", ERROR: "error" },
},
success: {
on: { RESET: "idle" },
},
error: {
on: { RETRY: "loading", RESET: "idle" },
},
},
}Thorough documentation of states is essential for consistent implementation. For each component in our design system, document:
| State | Visual Treatment | Behavior | Accessibility Considerations | Code Examples |
|---|---|---|---|---|
| Default | [Describe appearance] | [Describe behavior] | [List accessibility features] | [Provide code snippet] |
| Hover | Background lightens | Cursor changes to pointer | N/A | :hover selector |
| Focus | 2px blue outline | Responds to keyboard | Focus visible | [data-state="focused"] |
| Disabled | Gray appearance | No interaction | Communicates disabled status | [data-disabled="true"] |
For complex components, include a diagram showing all possible states and transitions between them. For example, a form submission process might follow this pattern:
Idle → Validating → Submitting → Success
↑ ↓ ↓
↑ ↓ ↓
↑--------↓------------↓
↑ ↓
Error ←-------------------
When designing components, consider:
When implementing states in code:
// Example implementation with proper state attributes
function Button({ children, disabled, isLoading, variant = "primary" }) {
return (
<button
className={`button ${variant}`}
disabled={disabled || isLoading}
data-state={isLoading ? "loading" : disabled ? "disabled" : "default"}
aria-busy={isLoading}
aria-disabled={disabled || isLoading}
>
{isLoading ? <LoadingSpinner /> : children}
</button>
)
}Consider a form component with multiple states. Here's how we might approach its implementation:
function ContactForm() {
const [state, setState] = useState("idle")
const [data, setData] = useState({ name: "", email: "", message: "" })
const [errors, setErrors] = useState({})
const handleSubmit = async (e) => {
e.preventDefault()
// Transition to validating state
setState("validating")
const validationErrors = validateForm(data)
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors)
setState("error")
return
}
// Transition to submitting state
setState("submitting")
try {
await submitFormData(data)
setState("success")
} catch (error) {
setErrors({ form: error.message })
setState("error")
}
}
// Render different UI based on current state
return (
<div className="form-container" data-state={state}>
{state === "success" ? (
<SuccessMessage />
) : (
<form onSubmit={handleSubmit}>
{/* Form fields here */}
{state === "error" && errors.form && (
<div className="error-message" role="alert">
{errors.form}
</div>
)}
<button
type="submit"
disabled={state === "submitting"}
data-state={state}
>
{state === "submitting" ? "Submitting..." : "Submit"}
</button>
</form>
)}
</div>
)
}