Spec Diff Browser
feedbackCustominteractive
Annotated single-column diff of a form spec. Renders the head tree with inline change badges, before/after rows for modified fields, and base-only entries spliced in for removed pages, groups, and fields. Tops the view with an overview strip of change counts and jump-links.
Custom component — no upstream reference.
Variants
Initial Import
These refs are identical.
1.Your Information
Personal Information3 fields, unchanged
First nametextrequired
Last nametextrequired
Middle nametext
<flex-spec-diff-browser class="flex-spec-diff-browser">
<div class="flex-spec-diff-browser__overview">
<p class="flex-spec-diff-browser__overview-empty">These refs are identical.</p>
</div>
<div class="flex-spec-diff-browser__panels">
<details id="page-page-1" class="flex-spec-diff-browser__panel" data-panel-kind="page" data-change="none" open="">
<summary class="flex-spec-diff-browser__panel-summary"><span class="flex-spec-diff-browser__panel-number">1.</span><span class="flex-spec-diff-browser__panel-title">Your Information</span></summary>
<div class="flex-spec-diff-browser__panel-body">
<div class="flex-spec-diff-browser__groups">
<details id="group-group-personal" class="flex-spec-diff-browser__group" data-change="none" open="">
<summary class="flex-spec-diff-browser__group-summary"><span class="flex-spec-diff-browser__group-title">Personal Information</span><span class="flex-spec-diff-browser__group-unchanged">3 fields, unchanged</span></summary>
<div class="flex-spec-diff-browser__group-body">
<div id="field-req-first-name" class="flex-spec-diff-browser__field" data-change="none">
<div class="flex-spec-diff-browser__field-row"><span class="flex-spec-diff-browser__field-label">First name</span><span class="flex-spec-diff-browser__field-type">text</span><span class="flex-spec-diff-browser__field-required">required</span></div>
</div>
<div id="field-req-last-name" class="flex-spec-diff-browser__field" data-change="none">
<div class="flex-spec-diff-browser__field-row"><span class="flex-spec-diff-browser__field-label">Last name</span><span class="flex-spec-diff-browser__field-type">text</span><span class="flex-spec-diff-browser__field-required">required</span></div>
</div>
<div id="field-req-middle-name" class="flex-spec-diff-browser__field" data-change="none">
<div class="flex-spec-diff-browser__field-row"><span class="flex-spec-diff-browser__field-label">Middle name</span><span class="flex-spec-diff-browser__field-type">text</span></div>
</div>
</div>
</details>
</div>
</div>
</details>
</div>
</flex-spec-diff-browser>No Changes
These refs are identical.
1.Your Information
Personal Information2 fields, unchanged
First nametextrequired
Last nametextrequired
<flex-spec-diff-browser class="flex-spec-diff-browser">
<div class="flex-spec-diff-browser__overview">
<p class="flex-spec-diff-browser__overview-empty">These refs are identical.</p>
</div>
<div class="flex-spec-diff-browser__panels">
<details id="page-page-1" class="flex-spec-diff-browser__panel" data-panel-kind="page" data-change="none">
<summary class="flex-spec-diff-browser__panel-summary"><span class="flex-spec-diff-browser__panel-number">1.</span><span class="flex-spec-diff-browser__panel-title">Your Information</span></summary>
<div class="flex-spec-diff-browser__panel-body">
<div class="flex-spec-diff-browser__groups">
<details id="group-group-personal" class="flex-spec-diff-browser__group" data-change="none">
<summary class="flex-spec-diff-browser__group-summary"><span class="flex-spec-diff-browser__group-title">Personal Information</span><span class="flex-spec-diff-browser__group-unchanged">2 fields, unchanged</span></summary>
<div class="flex-spec-diff-browser__group-body">
<div id="field-req-first-name" class="flex-spec-diff-browser__field" data-change="none">
<div class="flex-spec-diff-browser__field-row"><span class="flex-spec-diff-browser__field-label">First name</span><span class="flex-spec-diff-browser__field-type">text</span><span class="flex-spec-diff-browser__field-required">required</span></div>
</div>
<div id="field-req-last-name" class="flex-spec-diff-browser__field" data-change="none">
<div class="flex-spec-diff-browser__field-row"><span class="flex-spec-diff-browser__field-label">Last name</span><span class="flex-spec-diff-browser__field-type">text</span><span class="flex-spec-diff-browser__field-required">required</span></div>
</div>
</div>
</details>
</div>
</div>
</details>
</div>
</flex-spec-diff-browser>With Additions
1 added
Pages with changes: Your Information
1.Your Information
Personal Information
First nametextrequired
Last nametextrequired
Middle nametext+ New
<flex-spec-diff-browser class="flex-spec-diff-browser">
<div class="flex-spec-diff-browser__overview">
<p class="flex-spec-diff-browser__overview-counts"><span>1 added</span></p>
<p class="flex-spec-diff-browser__overview-jumps"><span class="flex-spec-diff-browser__overview-jumps-label">Pages with changes:</span> <a class="flex-spec-diff-browser__overview-jump" href="#page-page-1">Your Information</a></p>
</div>
<div class="flex-spec-diff-browser__panels">
<details id="page-page-1" class="flex-spec-diff-browser__panel" data-panel-kind="page" data-change="none" open="">
<summary class="flex-spec-diff-browser__panel-summary"><span class="flex-spec-diff-browser__panel-number">1.</span><span class="flex-spec-diff-browser__panel-title">Your Information</span></summary>
<div class="flex-spec-diff-browser__panel-body">
<div class="flex-spec-diff-browser__groups">
<details id="group-group-personal" class="flex-spec-diff-browser__group" data-change="none" open="">
<summary class="flex-spec-diff-browser__group-summary"><span class="flex-spec-diff-browser__group-title">Personal Information</span><span class="flex-change-indicator" data-variant="modified" role="status" aria-label="modified change"><span class="flex-change-indicator__dot" aria-hidden="true"></span></span></summary>
<div class="flex-spec-diff-browser__group-body">
<div id="field-req-first-name" class="flex-spec-diff-browser__field" data-change="none">
<div class="flex-spec-diff-browser__field-row"><span class="flex-spec-diff-browser__field-label">First name</span><span class="flex-spec-diff-browser__field-type">text</span><span class="flex-spec-diff-browser__field-required">required</span></div>
</div>
<div id="field-req-last-name" class="flex-spec-diff-browser__field" data-change="none">
<div class="flex-spec-diff-browser__field-row"><span class="flex-spec-diff-browser__field-label">Last name</span><span class="flex-spec-diff-browser__field-type">text</span><span class="flex-spec-diff-browser__field-required">required</span></div>
</div>
<div id="field-req-middle-name" class="flex-spec-diff-browser__field" data-change="added">
<div class="flex-spec-diff-browser__field-row"><span class="flex-spec-diff-browser__field-label">Middle name</span><span class="flex-spec-diff-browser__field-type">text</span><span class="flex-spec-diff-browser__badge" data-change="added">+ New</span></div>
</div>
</div>
</details>
</div>
</div>
</details>
</div>
</flex-spec-diff-browser>Contract
Documented variants
- NoChanges — Identical base and head specs: overview shows "These refs are identical"; all panels render from the head spec.
- WithAdditions — Head spec has a new field relative to base: overview shows the addition count; changed panel opens automatically.
- InitialImport — Null base specs (first-time import): revealMode defaults to "all" and every panel starts open.
Behavior promises
- ○ Overview strip counts changes by category and provides jump-links to changed pages
- ○ Panels with changes open automatically when revealMode is "changed"; unchanged panels collapse
- ○ Removed items from the base spec are spliced back into their original positions with a "Removed" badge
Source CSS
/* flex-spec-diff-browser — annotated single-column diff of a form spec.
One head-side browser with change badges inline, "was" rows for
modified/renamed fields, and base-side entries spliced in for removed
pages/groups/fields. Uses native <details> for all disclosure. */
flex-spec-diff-browser {
display: block;
container-type: inline-size;
container-name: spec-diff-browser;
}
.flex-spec-diff-browser {
display: flex;
flex-direction: column;
gap: var(--flex-space-md);
min-inline-size: 0;
font-size: 1rem;
line-height: 1.4;
}
/* Overview strip (counts + jump-links) */
.flex-spec-diff-browser__overview {
display: flex;
flex-direction: column;
gap: var(--flex-space-2xs);
padding: var(--flex-space-sm) var(--flex-space-md);
border: 1px solid var(--flex-color-border);
border-radius: var(--flex-radius-md);
background: var(--flex-color-surface);
}
.flex-spec-diff-browser__overview-empty {
margin: 0;
color: var(--flex-color-text-muted);
}
.flex-spec-diff-browser__overview-counts {
margin: 0;
font-weight: 600;
color: var(--flex-color-text);
font-size: 1rem;
}
.flex-spec-diff-browser__overview-jumps {
margin: 0;
color: var(--flex-color-text-muted);
}
.flex-spec-diff-browser__overview-jumps-label {
font-weight: 600;
color: var(--flex-color-text);
}
.flex-spec-diff-browser__overview-jump {
color: var(--flex-color-accent);
text-decoration: none;
&:hover,
&:focus-visible {
text-decoration: underline;
}
}
.flex-spec-diff-browser__overview-sep {
color: var(--flex-color-text-muted);
}
/* Panels (pages) */
.flex-spec-diff-browser__panels {
display: flex;
flex-direction: column;
gap: var(--flex-space-sm);
}
.flex-spec-diff-browser__panel {
border: 1px solid var(--flex-color-border);
border-radius: var(--flex-radius-md);
background: var(--flex-color-surface);
border-inline-start-width: 3px;
}
.flex-spec-diff-browser__panel[open] {
box-shadow: 0 1px 2px rgb(0 0 0 / 4%);
}
.flex-spec-diff-browser__panel-summary {
cursor: pointer;
list-style: none;
padding: var(--flex-space-sm) var(--flex-space-md);
display: flex;
align-items: center;
gap: var(--flex-space-sm);
flex-wrap: wrap;
font-weight: 600;
font-size: 1.06rem;
&::-webkit-details-marker {
display: none;
}
&::before {
content: "\25B8"; /* right-pointing triangle */
display: inline-block;
color: var(--flex-color-text-muted);
font-size: 0.85em;
transition: transform 0.15s ease;
flex-shrink: 0;
}
&:hover .flex-spec-diff-browser__panel-title {
text-decoration: underline;
}
&:focus-visible {
outline: var(--flex-focus-ring);
outline-offset: -2px;
}
}
.flex-spec-diff-browser__panel[open] > .flex-spec-diff-browser__panel-summary::before {
transform: rotate(90deg);
}
.flex-spec-diff-browser__panel-number {
color: var(--flex-color-text-muted);
font-variant-numeric: tabular-nums;
}
.flex-spec-diff-browser__panel-title {
flex: 1 1 auto;
}
.flex-spec-diff-browser__panel-body {
padding: 0 var(--flex-space-md) var(--flex-space-md);
display: flex;
flex-direction: column;
gap: var(--flex-space-sm);
border-block-start: 1px solid var(--flex-color-border);
padding-block-start: var(--flex-space-md);
min-inline-size: 0;
}
/* Change-category tints on pages / groups / fields */
.flex-spec-diff-browser__panel[data-change="added"],
.flex-spec-diff-browser__group[data-change="added"],
.flex-spec-diff-browser__field[data-change="added"] {
background: var(--flex-color-success-lighter);
border-inline-start-color: var(--flex-color-success);
}
.flex-spec-diff-browser__panel[data-change="modified"],
.flex-spec-diff-browser__panel[data-change="renamed"],
.flex-spec-diff-browser__group[data-change="modified"],
.flex-spec-diff-browser__group[data-change="renamed"],
.flex-spec-diff-browser__field[data-change="modified"],
.flex-spec-diff-browser__field[data-change="renamed"] {
background: var(--flex-color-warning-lighter);
border-inline-start-color: var(--flex-color-warning);
}
.flex-spec-diff-browser__panel[data-change="removed"],
.flex-spec-diff-browser__group[data-change="removed"],
.flex-spec-diff-browser__field[data-change="removed"] {
background: var(--flex-color-error-lighter);
border-inline-start-color: var(--flex-color-error);
}
.flex-spec-diff-browser__panel[data-change="moved"],
.flex-spec-diff-browser__group[data-change="moved"] {
background: var(--flex-color-info-lighter);
border-inline-start-color: var(--flex-color-info);
}
/* Strike the label on removed items so the "what disappeared" read lands. */
.flex-spec-diff-browser__field[data-change="removed"]
.flex-spec-diff-browser__field-label {
text-decoration: line-through;
color: var(--flex-color-text-muted);
}
/* Groups (inside a page panel) */
.flex-spec-diff-browser__groups {
display: flex;
flex-direction: column;
gap: var(--flex-space-sm);
}
.flex-spec-diff-browser__group {
border: 1px solid var(--flex-color-border);
border-radius: var(--flex-radius-sm);
background: var(--flex-color-surface);
border-inline-start-width: 3px;
}
.flex-spec-diff-browser__group-summary {
cursor: pointer;
list-style: none;
padding: var(--flex-space-xs) var(--flex-space-sm);
display: flex;
align-items: center;
gap: var(--flex-space-sm);
flex-wrap: wrap;
font-weight: 600;
font-size: 1rem;
&::-webkit-details-marker {
display: none;
}
&::before {
content: "\25B8";
display: inline-block;
color: var(--flex-color-text-muted);
font-size: 0.8em;
transition: transform 0.15s ease;
flex-shrink: 0;
}
&:focus-visible {
outline: var(--flex-focus-ring);
outline-offset: -2px;
}
}
.flex-spec-diff-browser__group[open]
> .flex-spec-diff-browser__group-summary::before {
transform: rotate(90deg);
}
.flex-spec-diff-browser__group-title {
flex: 1 1 auto;
}
.flex-spec-diff-browser__group-unchanged {
color: var(--flex-color-text-muted);
font-weight: 400;
}
.flex-spec-diff-browser__group-body {
padding: var(--flex-space-xs) var(--flex-space-sm) var(--flex-space-sm);
display: flex;
flex-direction: column;
gap: var(--flex-space-2xs);
border-block-start: 1px solid var(--flex-color-border);
padding-block-start: var(--flex-space-xs);
min-inline-size: 0;
}
/* Fields */
.flex-spec-diff-browser__field {
padding: var(--flex-space-xs) var(--flex-space-sm);
border-radius: var(--flex-radius-sm);
border-inline-start: 3px solid transparent;
display: flex;
flex-direction: column;
gap: var(--flex-space-2xs);
}
.flex-spec-diff-browser__field-row {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: var(--flex-space-xs);
font-size: 1rem;
}
.flex-spec-diff-browser__field-row--was {
color: var(--flex-color-text-muted);
font-size: 0.9rem;
text-decoration: line-through;
}
.flex-spec-diff-browser__field-was-prefix {
font-style: italic;
text-decoration: none;
}
.flex-spec-diff-browser__field-label {
color: var(--flex-color-text);
}
.flex-spec-diff-browser__field-type {
color: var(--flex-color-text-muted);
font-family: var(--flex-font-mono);
font-size: 0.85rem;
}
.flex-spec-diff-browser__field-required {
color: var(--flex-color-text-muted);
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 600;
}
/* Change badges (+ New, ~ Modified, etc.) */
.flex-spec-diff-browser__badge {
display: inline-block;
font-size: var(--flex-text-tag);
font-weight: 600;
padding: 0.125rem 0.5rem;
border-radius: var(--flex-radius-bubble);
line-height: 1.4;
white-space: nowrap;
&[data-change="added"] {
background: var(--flex-color-success);
color: var(--flex-color-surface);
}
&[data-change="modified"],
&[data-change="renamed"] {
background: var(--flex-color-warning);
color: var(--flex-color-text);
}
&[data-change="removed"] {
background: var(--flex-color-error);
color: var(--flex-color-surface);
}
&[data-change="moved"] {
background: var(--flex-color-info);
color: var(--flex-color-surface);
}
}
A digital services project by Flexion