State Machine
Jakub KrehelCommunityCreated by Jakub Krehel
- Skill
- Workflow
- Software development
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
The state-machine skill provides a structured process for isolating a single UI component and rendering it in every possible state on a temporary, throwaway page. By using mock data and a dedicated switcher, developers can toggle between different component states—such as loading, empty, error, or specific user roles—without modifying the component's source code. This approach ensures that visual changes can be tested across all variations of a component within the context of the actual application's layout and styles, while maintaining a clean codebase through a defined removal process.
What it does
- Identifies all reachable states of a component based on existing code branches.
- Creates a temporary scratch route that inherits the application's global styles and providers.
- Injects product-shaped mock data at the component's data boundary.
- Implements a switcher interface to toggle between component states via URL parameters.
- Ensures the component remains untouched by feeding data through props or existing seams.
- Provides a clear procedure for cleaning up the temporary page and fixtures after work is complete.
When to use it
- When you need to work on the visual appearance of a component across multiple states.
- When you want to verify how a component handles edge cases like empty, loading, or error states.
- When you need to explore how different user roles or feature flags affect component rendering.
- When you want to ensure visual changes do not inadvertently break other states of the same component.
Advantages
- Prevents the need to modify component code to accommodate testing fixtures.
- Maintains visual consistency by using the application's real layout and design tokens.
- Enables rapid iteration by keeping all states accessible via a single switcher.
- Ensures clean project maintenance through a standardized removal process.
Usage examples
- “Create a state machine for the UserProfileCard component.”
- The agent will identify the component, list its states (e.g., loading, active, suspended), create a temporary route, and provide a switcher to toggle between these states using mock data.
- “Remove the state machine page for the AuditLogTable.”
- The agent will delete the temporary page folder, remove any added data seams, and verify that no residual files or routes remain in the codebase.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/jakubkrehel/state-machine/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip state-machine.zip -d .agents/skills
Documentation by Jakub Krehel
State machine
This skill takes one component and renders it on a throwaway page in every state it can be in. Mock data feeds each state, a switcher flips between them and the page is deleted in one step when you are done.
It is a workbench, not a review. The page stays up while the user works on the visuals, one state on screen and every other a keypress away. Stacking hostile scenarios on one page for a single look is break, exploring alternative designs is variant and reviewing finished work is interface-review.
1. Scope one component
One component per run: the audit log table, the network panel, the billing card. "The settings page" spans several, so list them and ask which one.
Restate it in one sentence: what the component shows, where it renders in production and where its data comes from.
2. Find the states in the code
The states are the branches the component already has. Read it and its data hooks for every one:
| Kind | States |
|---|---|
| Data | Loading, empty, error, one item, a typical set, a set long enough to scroll or paginate, some requests resolved and others pending, stale data refetching |
| Account | Plan tier, role and permission checks, an owner versus a member |
| Feature | Flags, trials, limits reached, a disabled or locked feature |
A state the code cannot reach is not a state. Do not add a branch to render one. Where the design shows a state the code lacks, list it as missing and leave it out.
Write the set down before building, one line each, named the way the product talks about it: empty, enterprise, member-no-access. Group them under headings in the product's words, by what changes. Use Data and Account or narrower ones such as Connection or Access. The switcher shows these groups. Say which kinds you dropped and why in one line. Combine kinds only where the code branches on the combination, such as member-empty, never as a cross product.
3. Build the throwaway page
A scratch route inside the app holds the real component, imported from the project and untouched. The route inherits the app's layout, fonts, global styles and providers for free.
Where the framework splits server from client components, the page is client code, "use client" in Next. Otherwise fixture data can silently vanish crossing that boundary, and the page renders empty.
Render one instance in a container as wide as the component is in production. The container, the fixtures and the switcher are everything the page adds, with no fonts, styles or themes of its own. Keep the page, its fixtures and its switcher in one folder, such as /states/audit-log, so removal is one delete.
4. Feed each state at the data boundary
Supply the data where the component receives it, from the page, without editing the component. In order of preference:
Props, where the component takes its data as props.
The app's own data seam: a seeded query cache, a mock provider or the project's request mocks.
A fixture threaded through props five levels down tests a path production never takes. Where the only way in is editing the component, say so and ask before adding a seam.
Make the data look like the product. Real-shaped names, timestamps, amounts and the item counts users actually have. Three rows of "Test item" make every state look fine.
Loading and pending states hold still. Keep them pending until the switcher moves, rather than resolving after a timeout.
5. Add the switcher
A grouped list beside the component flips the __state search param, so every state is a link. Where there is no room beside the component, it moves above it as a select with the same groups. The double underscore keeps it clear of any param the app's layout reads. switcher.md holds the spec.
6. Confirm every state renders, then hand over
Load the page once in a browser already at hand and flip through every state. The component shows the mock data, not the real data and not a blank region. Mock data that never appears is the common failure here. Live sync, a cache that refetches over the seed or a server boundary dropping the fixture all cause it. Fix the plumbing before handing over.
With no browser at hand, say so and hand the URL over for the user to check.
Then hand over the URL, the state list and the switcher keys. Stop there. Changing how a state looks is the user's next request, not part of this one.
Re-check every state after each change
After a visual change, flip through every state again, since a fix for one state often breaks another.
Remove it in one step, on the user's word
Delete the page folder and any seam the user approved. Then search the codebase for the route and fixture names, and check that the diff touches nothing else of the setup.
Before you finish
| Mistake | Fix |
|---|---|
| The component edited to accept fixtures | Feed data from the page through props or the app's data seam; ask before adding a seam |
| A rebuilt lookalike component on the page | Import the real component from the project |
| The page restyles or re-themes the component | The app's layout, fonts and tokens as they are |
| Fixtures passed as props deep in the tree | Supply the data where the component receives it |
| A branch added to the component to show a state | Leave out states the code cannot reach, and list missing ones |
| "Item 1", "Test user", three rows | Product-shaped data in real quantities |
| Loading resolves after a timeout | Hold it until the switcher moves |
| Handed over without loading each state | Load every state; a blank or real-data state means broken plumbing |
| Switcher styled with the project's tokens | Keep it visibly outside the design system |
| One long row of tabs, one per state | A grouped list beside the component that scrolls on its own; a select above it where the list does not fit |
| Visuals changed unasked | Hand over and wait |
| Page deleted in the same turn it was built | Remove it only on the user's word |
| Route or fixture names left behind | Search for both after removal |
Files
- ATTRIBUTION.mdAdded by AutoGineer861 B
- LICENSELicence file added by AutoGineer1.0 KiB
- SKILL.md6.1 KiB
- agents/openai.yaml177 B
- switcher.md6.8 KiB
switcher.md
The switcher
The control that switches states. It sits beside the thing being judged, so build the spec below and leave it alone.
Deliberately outside the design system
Never style the switcher with the project's tokens, fonts or colors. One that looks native to the product becomes part of what you are looking at.
One dark neutral surface, the system font stack and no project variables. It does not follow the theme, because dark reads as chrome over both light and dark pages.
A grouped list, so it scales
States sit in a vertical list under the group headings from Find the states in the code. A row of tabs runs off the screen past about seven states; a grouped list stays scannable at thirty.
Every group has a heading, even when there is only one, and every heading carries the same small arrow icon.
States and groups keep the order they were written down in.
Behavior
It sets a
__statesearch param and reads the active state back from it. The URL is the source of truth, so every state is a link.The arrow keys, up and down or left and right, step through every state in list order, across groups.
Hhides and shows the switcher, list and select alike, for screenshots and screen recordings.Key handling ignores events with a modifier key, events already
defaultPreventedand events from inputs, textareas, selects and contenteditable elements.The active button carries
aria-pressed="true"and scrolls into view inside the list. The list carries a label, and each group is arole="group"labelled by its heading.The component switches instantly and keeps the scroll position. Only the active label eases its font weight.
The list and the select are both always rendered, and a container query shows one, so the active state survives a resize.
Structure
<div class="state-page">
<div class="state-layout">
<nav class="state-switcher" aria-label="States">
<!-- one group per heading -->
<div class="state-switcher-group" role="group" aria-labelledby="states-data">
<span class="state-switcher-heading" id="states-data">
<svg aria-hidden="true" viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v4a3 3 0 0 0 3 3h7" /><path d="m10 7 3 3-3 3" /></svg>
Data
</span>
<button type="button" data-state="loading" aria-pressed="false">Loading</button>
<button type="button" data-state="empty" aria-pressed="true">Empty</button>
</div>
</nav>
<div class="state-switcher-select">
<select aria-label="State">
<optgroup label="Data">
<option value="loading">Loading</option>
<option value="empty" selected>Empty</option>
</optgroup>
</select>
<svg aria-hidden="true" viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m4 6 4 4 4-4" /></svg>
</div>
<div class="state-stage">
<!-- the component, in a container as wide as it is in production -->
</div>
</div>
</div>Placement and styling
A container query on the page picks the layout:
Room for the list beside the component: two columns, the list then the component at its production width. The list is sticky and scrolls inside itself once it outgrows the viewport, so it stays put while you flip states.
No room: the select sits above the component, in the page flow. A phone opens its native menu for it, which suits a long list.
The page spans the full viewport width, because an app's layout often caps content near the component's own width. The query measures the page inside its padding, so the breakpoint is the list and the gap plus the component, which is 232px plus the production width. The CSS below uses a 644px component. Put the production width in max-width and the grid column, and set the query to 232px plus it.
The list's 6px padding keeps its own scrolling from clipping focus rings. The reset strips the select's native arrow, so the chevron is what shows it opens.
.state-page {
container: state-page / inline-size;
margin-inline: calc(50% - 50vw);
padding: 24px;
}
.state-layout {
display: flex;
flex-direction: column;
gap: 16px;
max-width: 644px;
margin-inline: auto;
}
.state-switcher {
all: unset;
display: none;
flex-direction: column;
gap: 16px;
max-height: calc(100dvh - 48px);
overflow-y: auto;
scrollbar-width: none;
padding: 6px;
border-radius: 14px;
background: rgb(20 20 20 / 0.92);
box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.1);
font: 13px/1 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
user-select: none;
}
.state-switcher-group {
display: flex;
flex-direction: column;
gap: 2px;
}
.state-switcher-heading {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 10px 6px;
font-size: 11px;
font-weight: 500;
color: rgb(255 255 255 / 0.45);
}
.state-switcher button {
all: unset;
flex: none;
padding: 8px 10px;
border-radius: 8px;
font: inherit;
color: rgb(255 255 255 / 0.6);
cursor: pointer;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: color 200ms ease-out;
}
.state-switcher button:hover {
color: rgb(255 255 255 / 0.85);
}
.state-switcher button[aria-pressed="true"] {
background: rgb(255 255 255 / 0.14);
color: rgb(255 255 255);
}
.state-switcher button:focus-visible {
outline: 2px solid rgb(255 255 255 / 0.7);
outline-offset: 2px;
}
.state-switcher-select {
position: relative;
align-self: flex-start;
max-width: 100%;
color: rgb(255 255 255);
}
.state-switcher-select select {
all: unset;
box-sizing: border-box;
max-width: 100%;
padding: 10px 36px 10px 16px;
border-radius: 999px;
background: rgb(20 20 20 / 0.92);
box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.1);
font: 500 13px/1 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
cursor: pointer;
}
.state-switcher-select select:focus-visible {
outline: 2px solid rgb(255 255 255 / 0.7);
outline-offset: 2px;
}
.state-switcher-select svg {
position: absolute;
top: 50%;
right: 14px;
translate: 0 -50%;
pointer-events: none;
color: rgb(255 255 255 / 0.6);
}
/* 208px list + 24px gap + 644px component */
@container state-page (min-width: 876px) {
.state-layout {
display: grid;
grid-template-columns: 208px 644px;
justify-content: center;
gap: 24px;
align-items: start;
max-width: none;
}
.state-switcher {
display: flex;
position: sticky;
top: 24px;
}
.state-switcher-select {
display: none;
}
}all: unset keeps the project's global button, nav and select styles out. In a framework, keep the class names and the structure and change only the rendering syntax.
Requirements and compatibility
Written for: Claude Code
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Claude Code frontmatter
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- Jakub Krehel
- Publisher
- Jakub Krehel (Community)
- Source
- jakubkrehel/skills · skills/state-machine · commit f41f027
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Jakub Krehel or Jakub Krehel.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download state-machine.zip