Migration Guide
Use DryUI as a presentation and accessibility system, not a workflow engine. Standardize on raw CSS grid, use Container for width, and keep the CLI lookup and validation loop in the process.
What DryUI owns
DryUI improves consistency, component correctness, and accessibility. It does not remove route complexity, domain orchestration, or state isolation work.
Presentation system
Use DryUI for fields, summaries, cards, alerts, transitions, and control semantics. Let your route code own session state, planner normalization, and cross-step orchestration.
One layout rule
All layout is scoped CSS grid. Use Container for constrained width and @container-based layout rules for responsive behavior.
Lookup-first workflow
Start with dryui info or dryui compose, then use check if your editor exposes DryUI MCP. Without MCP, rely on the
project build, tests, and the @dryui/lint preprocessor to catch drift.
Recommended workflow
The shortest stable path is lookup, implement, validate.
1. Lookup
Use dryui info or dryui compose to confirm simple vs compound shape, required parts, bindables, and the canonical usage
snippet. If MCP is available, ask is the equivalent surface.
2. Implement
Build the route with raw CSS grid, semantic tokens, and DryUI components for controls and stateful surfaces.
3. Validate
Run check on the component, theme CSS, directory, or workspace when DryUI
MCP is available so layout drift, compound misuse, and accessibility regressions get caught
before they spread. Without MCP, rebuild and run the normal test suite after wiring the
change.
If a component shape is uncertain, stop and look it up. DryUI is strict by design, and the lookup cost is lower than backing out a plausible but wrong abstraction.
State-heavy planners
Dependent-field flows still need disciplined route state. Normalize first, then render DryUI.
This pattern keeps dependent selects honest: when the parent value changes, derived options update and stale child selections are cleared. DryUI owns the field surface; your route code owns the state transition.
<script lang="ts">
let country = $state('');
let airport = $state('');
const airportOptions = $derived(getAirports(country));
$effect(() => {
if (!airportOptions.some((option) => option.value === airport)) {
airport = '';
}
});
</script>
<div class="planner">
<Field.Root>
<Label>Country</Label>
<Select.Root bind:value={country}>
<Select.Trigger><Select.Value placeholder="Choose country" /></Select.Trigger>
<Select.Content>{/* items */}</Select.Content>
</Select.Root>
</Field.Root>
<Field.Root>
<Label>Airport</Label>
<Select.Root bind:value={airport} disabled={!country}>
<Select.Trigger><Select.Value placeholder="Choose airport" /></Select.Trigger>
<Select.Content>{/* filtered items */}</Select.Content>
</Select.Root>
</Field.Root>
</div>
<style>
.planner {
display: grid;
gap: var(--dry-space-4);
}
</style>