Branch Switcher
navigationCustominteractive
Dropdown for switching between branches with a filter and an inline create-branch form.
Custom component — no upstream reference.
Variants
Default
- main
- story-1/intake-form3 ahead
<flex-branch-switcher class="flex-branch-switcher">
<button type="button" class="flex-branch-switcher__trigger" aria-haspopup="listbox" aria-expanded="false">
<svg aria-hidden="true" class="flex-branch-switcher__icon" width="14" height="14" viewBox="0 0 16 16">
<path d="M9.5 3.25a2.25 2.25 0 1 1 3 2.122V6A2.5 2.5 0 0 1 10 8.5H6a1 1 0 0 0-1 1v1.128a2.251 2.251 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.5 0v1.836A2.5 2.5 0 0 1 6 7h4a1 1 0 0 0 1-1v-.628A2.25 2.25 0 0 1 9.5 3.25Z">
</path>
</svg>
<span class="flex-branch-switcher__current">main</span>
<span class="flex-branch-switcher__badge">published</span>
<span aria-hidden="true" class="flex-branch-switcher__caret">▾</span>
</button>
<div class="flex-branch-switcher__panel" role="listbox" hidden="">
<input type="search" class="flex-branch-switcher__filter" placeholder="Find a branch..."/>
<ul class="flex-branch-switcher__list">
<li class="flex-branch-switcher__option" role="option" aria-selected="true"><a class="flex-branch-switcher__link" href="/main/">main</a></li>
<li class="flex-branch-switcher__option" role="option" aria-selected="false"><a class="flex-branch-switcher__link" href="/story-1/intake-form/">story-1/intake-form</a><span class="flex-branch-switcher__ahead">3 ahead</span></li>
</ul>
<form method="post" action="/branches/create" class="flex-branch-switcher__create">
<input class="flex-text-input flex-branch-switcher__create-input" name="name" placeholder="new-branch-name" required=""/>
<button type="submit" class="flex-button flex-branch-switcher__create-submit">Create branch</button>
</form>
</div>
</flex-branch-switcher>On Feature Branch
- main
- story-1/intake-form3 ahead
- story-2/pdf-extract1 ahead
<flex-branch-switcher class="flex-branch-switcher">
<button type="button" class="flex-branch-switcher__trigger" aria-haspopup="listbox" aria-expanded="false">
<svg aria-hidden="true" class="flex-branch-switcher__icon" width="14" height="14" viewBox="0 0 16 16">
<path d="M9.5 3.25a2.25 2.25 0 1 1 3 2.122V6A2.5 2.5 0 0 1 10 8.5H6a1 1 0 0 0-1 1v1.128a2.251 2.251 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.5 0v1.836A2.5 2.5 0 0 1 6 7h4a1 1 0 0 0 1-1v-.628A2.25 2.25 0 0 1 9.5 3.25Z">
</path>
</svg>
<span class="flex-branch-switcher__current">story-1/intake-form</span>
<span class="flex-branch-switcher__trigger-meta">3 ahead</span>
<span aria-hidden="true" class="flex-branch-switcher__caret">▾</span>
</button>
<div class="flex-branch-switcher__panel" role="listbox" hidden="">
<input type="search" class="flex-branch-switcher__filter" placeholder="Find a branch..."/>
<ul class="flex-branch-switcher__list">
<li class="flex-branch-switcher__option" role="option" aria-selected="false"><a class="flex-branch-switcher__link" href="/main/">main</a></li>
<li class="flex-branch-switcher__option" role="option" aria-selected="true"><a class="flex-branch-switcher__link" href="/story-1/intake-form/">story-1/intake-form</a><span class="flex-branch-switcher__ahead">3 ahead</span></li>
<li class="flex-branch-switcher__option" role="option" aria-selected="false"><a class="flex-branch-switcher__link" href="/story-2/pdf-extract/">story-2/pdf-extract</a><span class="flex-branch-switcher__ahead">1 ahead</span></li>
</ul>
<form method="post" action="/branches/create" class="flex-branch-switcher__create">
<input class="flex-text-input flex-branch-switcher__create-input" name="name" placeholder="new-branch-name" required=""/>
<button type="submit" class="flex-button flex-branch-switcher__create-submit">Create branch</button>
</form>
</div>
</flex-branch-switcher>Contract
Documented variants
- Default — Switcher trigger showing the main branch with a "published" badge; dropdown panel is hidden.
- OnFeatureBranch — Switcher trigger showing a feature branch with an "ahead" count; multiple branches in the list.
Behavior promises
- ○ Clicking the trigger button opens the dropdown panel by toggling aria-expanded and removing the hidden attribute
- ○ Filtering the search input narrows the branch list to matching entries
- ○ Submitting the create-branch form with a valid name navigates to the new branch
Source CSS
/* flex-branch-switcher — dropdown for selecting the active branch,
filtering the list, and creating a new branch inline. */
flex-branch-switcher {
position: relative;
display: inline-block;
}
.flex-branch-switcher__trigger {
display: inline-flex;
align-items: center;
gap: var(--flex-space-xs);
padding: var(--flex-space-xs) var(--flex-space-sm);
border: 1px solid var(--flex-color-border);
border-radius: var(--flex-radius-sm);
background: var(--flex-color-surface);
color: var(--flex-color-text);
font-family: var(--flex-font-mono);
font-size: var(--flex-text-base);
cursor: pointer;
}
.flex-branch-switcher__trigger:hover {
background: var(--flex-color-bg-subtle);
}
.flex-branch-switcher__trigger:focus-visible {
outline: var(--flex-focus-ring);
outline-offset: var(--flex-focus-offset);
}
.flex-branch-switcher__current {
font-weight: 600;
}
.flex-branch-switcher__caret {
color: var(--flex-color-text-muted);
}
.flex-branch-switcher__panel {
position: absolute;
inset-block-start: calc(100% + var(--flex-space-xs));
inset-inline-start: 0;
z-index: 10;
min-inline-size: 16rem;
padding: var(--flex-space-sm);
border: 1px solid var(--flex-color-border);
border-radius: var(--flex-radius-md);
background: var(--flex-color-surface);
box-shadow: var(--flex-shadow-md);
}
.flex-branch-switcher__panel[hidden] {
display: none;
}
.flex-branch-switcher__filter {
box-sizing: border-box;
inline-size: 100%;
margin-block-end: var(--flex-space-sm);
padding: var(--flex-space-xs) var(--flex-space-sm);
border: 1px solid var(--flex-color-border);
border-radius: var(--flex-radius-sm);
font-family: var(--flex-font-sans);
font-size: var(--flex-text-base);
}
.flex-branch-switcher__list {
max-block-size: 16rem;
margin: 0;
padding: 0;
overflow-y: auto;
list-style: none;
}
.flex-branch-switcher__option {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--flex-space-sm);
padding: var(--flex-space-xs) var(--flex-space-sm);
border-radius: var(--flex-radius-sm);
}
.flex-branch-switcher__option:hover {
background: var(--flex-color-bg-subtle);
}
.flex-branch-switcher__option[aria-selected="true"] {
background: var(--flex-color-info-lighter);
}
.flex-branch-switcher__link {
flex: 1;
color: var(--flex-color-text);
font-family: var(--flex-font-mono);
font-size: var(--flex-text-base);
text-decoration: none;
}
.flex-branch-switcher__link:hover {
text-decoration: underline;
}
.flex-branch-switcher__ahead {
color: var(--flex-color-text-muted);
font-family: var(--flex-font-sans);
font-size: var(--flex-text-xs);
}
.flex-branch-switcher__create {
display: flex;
gap: var(--flex-space-xs);
margin-block-start: var(--flex-space-sm);
padding-block-start: var(--flex-space-sm);
border-block-start: 1px solid var(--flex-color-border);
}
.flex-branch-switcher__create-input {
flex: 1;
box-sizing: border-box;
min-inline-size: 0;
padding: var(--flex-space-xs) var(--flex-space-sm);
border: 1px solid var(--flex-color-border);
border-radius: var(--flex-radius-sm);
font-family: var(--flex-font-mono);
font-size: var(--flex-text-base);
}
.flex-branch-switcher__create-submit {
padding: var(--flex-space-xs) var(--flex-space-sm);
border: 1px solid var(--flex-color-accent);
border-radius: var(--flex-radius-sm);
background: var(--flex-color-accent);
color: var(--flex-color-on-accent);
font-size: var(--flex-text-base);
cursor: pointer;
}
.flex-branch-switcher__create-submit:hover {
background: var(--flex-color-link-hover);
border-color: var(--flex-color-link-hover);
}
.flex-branch-switcher__icon {
flex-shrink: 0;
fill: currentcolor;
}
.flex-branch-switcher__badge {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 1px 0.4rem;
border-radius: var(--flex-radius-sm);
background: var(--flex-color-warning-lighter);
color: var(--flex-color-text);
}
.flex-branch-switcher__trigger-meta {
font-size: 0.82rem;
color: var(--flex-color-text-muted);
font-weight: 400;
}
A digital services project by Flexion