Specdeck is a tool for making your specs easier to manage in highly stateful applications. Specdeck does this by enforcing a consistent hierarchical container structure and doing basic validations.
It is highly recommended to maintain a Specdeck project lives in a git repository. It defines the possible states of an application (combinations of facts like language, subscription tier, or experiment enrollment) and organises containers — a nestable hierarchy of components — each with specs that vary per state.
Initialise a new project in an empty git repository:
specdeck new [name]If no name is provided, the directory name is used. This creates:
specdeck.toml # project configuration
states/ # state and fact definitions
containers/ # component hierarchy
dist/ # build output
Facts are the atomic properties that describe a state. They live in states/facts/ and are either boolean or enum typed.
# states/facts/language.yml
name: language
type: enum
values: [en, es, fr, de]# states/facts/is-logged-in.yml
name: is-logged-in
type: booleanStates are defined in YAML files under states/, grouped by domain or feature area. Each file contains a list of states that reference facts.
# states/checkout.yml
- name: default
summary: Standard logged-in English user going through checkout
facts:
language: en
is-logged-in: true
- name: spanish-premium-checkout
summary: Spanish-speaking premium user in the new checkout experiment
facts:
language: es
subscription: premium
experiment: checkout-v2State names are unique across all state files. The state named default is the baseline state unless overridden (see below).
Containers are a nestable hierarchy that mirrors your application's component structure — app, tabs, screens, widgets. They live in containers/ as a directory tree.
- Leaf containers are
.ymlfiles and hold specs. - Non-leaf containers that also carry specs use an
index.ymlfile alongside their subdirectories.
containers/
app/
index.yml # specs for the app container
home-tab/
feed-screen/
post-card.yml # specs for the post card component
All specs must be defined within a state reference. There is no implicit inheritance between states — each state's spec set is its complete, self-contained truth.
# containers/app/home-tab/feed-screen/post-card.yml
states:
- ref: default
specs:
background-color: "#FFFFFF"
title-text-color: "#000000"
title-text: "Latest posts"
- ref: spanish-premium-checkout
specs:
background-color: "#FFFFFF"
title-text-color: "#000000"
title-text: "Últimas publicaciones"Specs can be defined in shorthand (string value) or verbose form (with an optional description):
specs:
background-color: "#FFFFFF"
title-text:
value: "Latest posts"
description: Heading shown at the top of the feedBy default, the state named default is the baseline. To use a differently named state as the baseline, declare it at the top of the container file:
default: baseline
states:
- ref: baseline
specs:
background-color: "#FFFFFF"
- ref: dark-mode
specs:
background-color: "#1A1A1A"Top-level specs: (not nested under a state) are shorthand for the default state:
# these specs implicitly belong to the 'default' state
specs:
background-color: "#FFFFFF"
title-text: "Hello"A container can import other containers by reference. The built output groups each import as a named section, titled from the imported container's title field (falling back to the filename stem).
# containers/jot/index.yml
containers:
- $ref: '../shared/hero.yml'
- $ref: './noteInput.yml'Containers declare the interactions they support under an events: key. Each event has a trigger name (title), an optional description, and one or more typed actions nested under actions:.
events:
- title: on-press-enter
description: user presses enter
actions:
navigate:
destination: /jot/tagsMultiple actions per event are supported:
events:
- title: on-press-submit
actions:
navigate:
destination: /home
track:
event: form_submittedBuilt-in action types
| Action | Purpose | Fields |
|---|---|---|
navigate |
route change | destination (required), transition (optional: push / modal / replace) |
track |
analytics / logging | event (required), properties (optional map) |
dispatch |
trigger a background job | job (required), payload (optional map) |
update |
mutate local state | target (required), value (required) |
open |
external URL or deep link | url (required) |
dismiss |
close the current view | — |
prompt |
show a dialog or alert | message (required), confirm_action (optional) |
Custom action types
Because the action key is the type, any string is a valid action type — there is no allowlist. Custom types pass through as-is and render alongside built-ins:
events:
- title: on-press-enter
actions:
haptic:
pattern: heavy
navigate:
destination: /homeTeams can freely define a shared vocabulary of custom types (e.g. haptic, toast, permission-request) and use them consistently across containers. The payload fields are arbitrary — specdeck does not validate them for custom types.
Initialise a new project in the current directory (must be a git repo). Infers the project name from the directory if not provided.