Forms and Interaction
Patterns for form composition, input states, auto-save behavior, chart styling, and naming conventions.
Building Forms
The standard pattern for forms uses Section as the container and InputField as the input wrapper. Do not use raw Tier 1 primitives directly — always use InputField or CheckboxRow.
InputField Layouts
InputField supports two layouts and auto-detects the input type from props:
{/* Stacked layout (default) — label above input */}
<Section title="Dam Overview">
<div className="flex flex-col gap-4">
<InputField label="Year Completed" inputType="number" align="left" value={year} onValueChange={setYear} />
<InputField label="Dam Height (ft)" decimals={1} value={height} onValueChange={setHeight} />
<InputField label="Dam Type" options={damTypeOptions} value={type} onChange={setType} />
</div>
</Section>
{/* Grid layout — multiple fields in columns */}
<Section title="Embankment Overtopping Parameters">
<div className="grid grid-cols-2 gap-x-4 gap-y-4">
<InputField label="Embankment Height (ft)" inputType="number" inputVariant="compact" ... />
<InputField label="Downstream Slope (H:V)" inputType="number" inputVariant="compact" ... />
<InputField label="Crest Width (ft)" inputType="number" inputVariant="compact" ... />
<InputField label="Tailwater Depth (ft)" inputType="number" inputVariant="compact" ... />
</div>
</Section>
Use inputVariant="compact" for dense grid layouts. Use inputVariant="default" (or omit) for standard spacing.
CheckboxRow Patterns
CheckboxRow is used for toggle-with-optional-input patterns:
<Section title="Embankment Components" hint="(check all that apply)" contentClassName="space-y-2">
<CheckboxRow
label={
<>
Conduit through Embankment <InfoPopover title="Conduit">...</InfoPopover>
</>
}
checked={components.hasConduit}
onChange={(e) => onComponentChange('hasConduit', e.target.checked)}
/>
<CheckboxRow
label="Vertical or Inclined Drain/Filter"
checked={components.hasVerticalDrain}
onChange={(e) => onComponentChange('hasVerticalDrain', e.target.checked)}
{...(components.hasVerticalDrain && {
inputType: 'number',
decimals: 1,
align: 'left',
inputValue: components.verticalDrainElevation,
onInputValueChange: (v) => onComponentChange('verticalDrainElevation', v),
inputPlaceholder: 'Top Elevation (ft)',
inputVariant: 'xs',
inputClassName: 'ml-4 w-40',
})}
/>
</Section>
Key patterns:
- Use
contentClassName="space-y-2"on the Section for tight vertical spacing - Use conditional spread syntax
{...(condition && props)}to show/hide the inline input - The
disableInputWhenUncheckedprop auto-disables the input when the checkbox is unchecked
Form Input States
| State | Border | Ring | Usage |
|---|---|---|---|
| Default | border-dst-border | none | Normal state |
| Focus | border-dst-steel | ring-1 ring-dst-focus-ring | Active editing |
| Invalid | border-rose-500 | ring-2 ring-rose-500 | Validation error |
| Warning | border-amber-500 | ring-1 ring-amber-500 | Out-of-bounds warning |
| Disabled | bg-dst-disabled-bg | none | Not editable |
These states are built into the Tier 1 input primitives (InputNumber, InputText, Select) and are inherited by InputField and CheckboxRow. You don't need to implement them manually.
Auto-Save Pattern
The application uses debounced auto-save for user inputs. No "Save" button for normal parameter entry.
- Standard Fields
- Text Areas
- Explicit Actions
- Indicators
- Save on change with 500ms debounce
- Use
useDebouncehook fromhooks/common/useDebounce.js - Track changes with
userEditedRefto prevent duplicate saves - Use hash comparison to skip saves when data hasn't changed
- Save on blur instead of debounced change
- Prevents saving partial text while the user is typing
- Modal "Save" buttons for rename operations
- Never require a "Save" button for normal parameter entry
- A small "Saving..." indicator may appear during saves
- The UI remains interactive (non-blocking)
- On success, the indicator disappears
- On error, an error message appears
Chart Styling
Charts use Chart.js with consistent styling. All chart colors are resolved from CSS custom properties at render time.
import { getChartColors, FONT_FAMILY } from '@components/common/chart/utils';
const C = getChartColors();
// Axis styling
ticks: { color: C.text, font: { family: FONT_FAMILY, size: 15 } }
grid: { color: C.gridMinor, drawBorder: false }
title: { color: C.text, font: { family: FONT_FAMILY, size: 16 } }
// Tooltip styling
tooltip: {
backgroundColor: C.tooltipBg,
borderColor: C.tooltipBorder,
borderWidth: 1,
titleColor: C.text,
bodyColor: C.text,
}
Font family: 'Aptos Narrow, system-ui, sans-serif' (from the FONT_FAMILY constant)
Line colors: Use C.primary (steel blue) for DST estimates, C.revised (amber) for user-modified values.
Naming Conventions
| Type | Convention | Example |
|---|---|---|
| Components | PascalCase | HydrologicHazardTable |
| Custom hooks | use* prefix | useHydrologicHazardData |
| Functions/Variables | camelCase | calculateRisk, userName |
| Constants | SCREAMING_SNAKE_CASE | MAX_RETRIES, DEBOUNCE_MS |
| Visual constants | SCREAMING_SNAKE_CASE | ACCENT_BAR, HEADING |
| Event handler props | on* prefix | onClick, onSubmit, onValueChange |
| CSS tokens | kebab-case with dst- prefix | --color-dst-steel, bg-dst-page-bg |
For the complete naming conventions across all languages (C#, JavaScript, Python, R), see Case Conventions by Language.
Do's and Don'ts
- Do
- Don't
- Use
Sectionfor page sections - Use
InputFieldfor form inputs (not rawInputNumber/InputText) - Use
CheckboxRowfor checkbox + label + optional input patterns - Use Tailwind's
slatescale for neutrals - Declare visual constants at the top of component files
- Use
dst-*tokens for brand colors - Test at all 5 reference device sizes
- Keep page components thin — extract complex JSX to sub-components
- Add
min-w-0to grid children to prevent overflow
- Use legacy layout wrappers — use
Sectioninstead - Use raw Tier 1 primitives (
InputNumber,InputText,Select) directly in page layouts — wrap them inInputField - Use arbitrary hex values in components (
text-[#94a3b8]) - Use red for anything other than errors/warnings/USACE branding
- Put business logic in page components
- Create custom tokens for neutrals (use
slate-*) - Skip the visual constants pattern
- Use inline styles — use Tailwind classes