Sketching States Before Screens #
Exploring how plain-text state sketching and behavior-first design help clarify complex UI interactions before visual design, improving collaboration and product flow.
After a few years away from publishing, I’m rebooting this site with something I’ve been quietly exploring: a plain-text way to sketch product flows and UI behavior before jumping into visual design.
This started a while back when I was designing a handheld Android barcode scanner used by warehouse workers. The interface was straightforward, but the system logic wasn’t. I kept hitting the same question: “What happens now? What happens next?”
Statecharts helped me map out the complexity — user input, errors, retries, edge cases — but the tooling felt scattered. I had diagrams in one place, design files in another, and notes everywhere. I tried Sketch.systems and other tools, but I still ended up stitching things together manually.
Visual design tools are great at showing how an interface looks — but not how it behaves.
And yet, behavior is where so many product decisions live. These moments — when something fails, changes, or surprises the user — shape the actual experience.
State thinking helps expose those questions early. It gives designers and developers a shared language for what the app does, not just what it shows.
So I started tinkering with a lightweight DSL (domain-specific language) to describe app behavior in text. Something expressive but not overly technical. Here's a simplified example:
Repository Page
    keypress "/" -> CommandPalette:active
    
    CommandPalette
    query_scope: repo:owner/repo
    
        Inactive*
        
        Active&
            type "test" -> Suggestions / "Jump to" updated w/ params
            keypress "Enter" => SERP w/ query_scopeThis lets me describe screens, components, parameters, nested states, and user interactions in a clear and readable way — kind of like writing a narrative sequence like a screenplay.
I’ve also been converting this DSL into a flattened XState machine that works with the Stately visualizer. Seeing the behavior mapped out visually helps me validate the logic early, even before I start drawing screens.
Here’s how the DSL elements map to XState structures:
| DSL Element | DSL Syntax | XState Representation | 
|---|---|---|
| Page / Screen | Repository Page | Top-level state (e.g., RepositoryPage) | 
| Component | CommandPalette | Nested state inside parent (e.g., RepositoryPage.states.CommandPalette) | 
| Parameter | query_scope: repo:owner/repo | Stored in context | 
| Initial State | Inactive* | initial: 'Inactive' | 
| Parallel State | Active& | type: 'parallel' inside XState | 
| User Action / Event | keypress "/" | Event (e.g., KEYPRESS_Slash) | 
| Transition | -> | Internal transition (e.g., on: { KEYPRESS_Slash: ... }) | 
| Navigation | => | target with absolute path or external screen state | 
| Nested Selector | Suggestions / "Jump to" | Nested state paths | 
And here’s a matching snippet from the XState machine:
import { createMachine } from 'xstate';
const appMachine = createMachine({
  id: 'App',
  initial: 'RepositoryPage',
  states: {
    RepositoryPage: {
      id: 'RepositoryPage',
      initial: 'CommandPalette',
      states: {
        CommandPalette: {
          id: 'CommandPalette',
          initial: 'Inactive',
          states: {
            Inactive: {
              on: {
                KEYPRESS_Slash: 'Active'
              }
            },
            Active: {
              type: 'parallel',
              states: {
                Suggestions: {
                  id: 'Suggestions',
                  initial: 'JumpTo',
                  states: {
                    JumpTo: {
                      id: 'JumpTo',
                      initial: 'Idle',
                      states: {
                        Idle: {
                          on: {
                            TYPE_test: 'UpdatedWParams'
                          }
                        },
                        UpdatedWParams: {
                          id: 'UpdatedWParams',
                          on: {
                            SELECT_helper_js: '#App.FilePage_helper_js'
                          }
                        }
                      }
                    }
                  }
                }
              },
              on: {
                KEYPRESS_Enter: '#App.SERP_w_query_scope'
              }
            }
          }
        }
      }
    },
    SERP_w_query_scope: {
      type: 'final'
    },
    FilePage_helper_js: {
      type: 'final'
    }
  }
});
This isn’t a product or a full-blown tool — at least not yet. I’m not a developer, so most of this is prototyped by hand. But I’m curious where it might go.
Some ideas I’m thinking about:
Interactive prototypes that simulate the state machine behavior
A web editor that lets you type the DSL and preview the flow
Figma plug-ins or integrations that bring state logic into screen design
For now, it’s just a side project — but one that’s helped me think more clearly about interaction design.
If you’re a designer, developer, or curious hybrid who’s felt limited by UI-first tools, I’d love to hear what you’ve tried — or what you’ve wished existed.
Acknowledgments #
This exploration builds on the work and ideas of others who’ve helped me think differently about interaction design:
- Ryan Singer — for his shorthand approach to UI flows that values behavior before visuals
 - Kevin Lynagh — whose work on Sketch.systems first showed me the power of plain-text state modeling
 - David Khourshid — for XState and the broader movement to bring clarity and visual tooling to complex state machines
 
Big thanks to them for pushing the field forward.
🙏🙏🙏
Since you've made it this far, sharing this article on your favorite social media network would be highly appreciated 💖! For feedback, please ping me on Twitter.
Published