Changelog
shadcn add copies source into your tree. From that moment your file and ours have unrelated histories — there is no npm update to carry a fix across. So this page is not a courtesy: it is the upgrade path.
Which version am I holding?
Every installed file carries a version banner a few lines in. To check a whole project against this registry at once:
# in your project — every installed file states its own version
grep -h "^// @interlace/" src/components/ui/*.tsx
# what this registry ships right now, per item
curl -s https://ds.interlace.tools/data/agent-index.json | jq -r '.items[] | "\(.name) \(.version)"'A file with no banner was installed before the banner shipped, or had its leading comments stripped by an older CLI — in that case we genuinely cannot tell which version you hold, and re-adding the item is the way to adopt one you can track. Compare versions, never content: you are expected to edit your copy, and a check that flagged every deliberate customisation as drift would be ignored within a week.
A packaged one-command form of this (consumer:check in the registry repo) reports every file as current, behind, unversioned or local and exits non-zero when anything is stale. It is not published to npm yet.
How to read an entry
- Breaking
- Your copy will not behave the same. Every breaking entry carries a migration note naming the edit you make in your own file.
- Added
- New components, new props, new tokens. Re-run the install for the components you want; nothing you own changes underneath you.
- Changed
- A fix or an internal change with the same public surface. Safe to re-install; safe to skip.
Each component also carries its own version, stamped into the file you install as a banner. The full contract — what counts as breaking for source you already own — is in VERSIONING_PHILOSOPHY.md.
v1.1.0
10 entries- Addedminor
Absence becomes a first-class value.
DataStategrows from four states to nine, and two new primitives consume the union as a contract rather than each re-deriving it from a boolean ladder. The states areloading · error · not-applicable · not-counted · empty · partial · truncated · first-measurement · idle, and that list is also the precedence order — lowest wins. Error beats empty, because a failed fetch is not "no results". Truncated is not empty at all: it qualifies a body that still renders. States co-occur, soresolveDataStatereturns the winner AND every loser asqualifiers, the wrapper publishes them asdata-qualifiers, and the announcement says all of them — a list that is both partially covered and truncated is wrong twice and now says so twice. Each state is drawn distinctly and, more importantly, **announced**. A diagonal hatch means no run happened; a dashed outline means not yet real; the accent is spent onfirst-measurementalone, because it is the only absence a reader can act on. Every one of those carries a sentence viaannounceDataState— "Not counted. No measurement was taken; this is not a zero." A hatch that exists only in pixels keeps the distinction for sighted readers and destroys it for everyone else, and axe scores that green.StatStripis the dense measurement grid that appeared in all six of the audited artifacts, hand-rolled four times: mono micro-label,tabular-numsvalue, optional note, optional delta, on a<dl>so every number keeps its label. It renders the three-state null honestly — a metric withprior: nullshows "first measurement" and the caller's delta node is **not rendered at all**, so there is no path by which a missing prior reaches the DOM as+0%.Meterabsorbs three hand-rolled bars (an odds bar, a score meter, a reviewer bar). Magnitude is carried by length AND by the printed number, never by hue, and the number is not optional.value: nullhatches by default rather than drawing an empty bar, because an empty bar and a measured zero are the same picture.variant="dead"recedes without vanishing, for a dormant row that still holds its rank.RankedBarListis the repeated-row composition and supports a labelled log scale, so a 10k row and a 10M row can share an axis. Pure logic ships alongside asdata-state-modelandmeter-scale— the union, the precedence, the announcements, the scale maths — with no React import, exported for callers wiring their own state.Skeletongainsmeterandstat-stripvariants, both composites, so the loading state reserves the real silhouette on the real breakpoints. Not included, deliberately: the domain vocabularies these were extracted from (authority,visibility,dormant,unwritten,ungated) stay in their artifacts as labels a caller passes in. They are subject-matter, not design system — the DS ships the two axes underneath them (does it recede or does it act, and did a run happen) rather than nine nouns it cannot define. - Addedminor
New
ComboboxandCommandPalette.ComboboxmirrorsSelectpart-for-part, plus one part that is ours rather than Base UI's:ComboboxControl, arelativerow. Base UI has no concept of "the input plus the affordances docked inside its border", and without itComboboxClearandComboboxTriggersit beside the field instead of in it. The root is aliased rather than wrapped so the<Value, Multiple>generics survive at the call site.CommandPaletteis a composition, not a new primitive:DialogContentsupplies the portal, backdrop, focus trap, Escape and focus restore, wrapped aroundCombobox.Rootin **inline** mode. That flag is load-bearing — without it the combobox handles Escape for a popup that is not open and the dialog never closes, a WCAG 2.1.2 keyboard trap.closeOnSelectcloses by clicking a hiddenDialogCloserather than callingsetOpen, so it routes through Base UI's own close path:onOpenChangefires with a real reason, focus restores exactly as for Escape, and the uncontrolled case works. The ⌘K binding is a separate opt-in hook. A design system should not seize a global chord on mount from every app that installs it. Three rows of the keyboard contract differ fromSelectand are the ones callers get wrong: Home/End move the text caret rather than the highlight; Escape on a _closed_ popup clears the input and the selection; focus never enters the list — it is virtual focus viaaria-activedescendant. - Addedminor
New
DataTablepattern — a real<table>with column definitions, sorting, row selection, pagination, and designed loading / empty / error states. Sort and selection are the caller's state (sort/onSortChange,selected/onSelectionChange), so the whole view can live in the query string and a sorted, paged, partly-selected table can be linked to a colleague. Selection is keyed by row id, so it survives sorting and paging. Semantics are the point:<caption>,<th scope="col">, one<th scope="row">per row,aria-sorton sortable headers, and a selection checkbox whose accessible name identifies the row. Pure logic (sort cycle, comparators, page-window, selection maths) ships alongside asdata-table-modeland is exported for callers wiring their own URL state. Skeleton gains adata-tablevariant — a header row plus body rows, so the loading state reserves the real silhouette instead of collapsing to a spinner. Not included, deliberately: filtering UI, column resize/reorder, virtualization, grouping.Components - Addedminor
New
Distributionchart — a quantity spread over a fixed set of named bins, read against a reference distribution. It is a new component rather than an extension ofTimeSeriesbecause three of that component's properties are chronological and cannot be parameterised out: its axis keys are sorted (['Thu','Fri','Sat']sorts into a week that does not exist), a line asserts the metric passed through every value between two samples when between two hourly aggregates there is nothing to pass through, anddelta()first-to-last is arithmetic performed on a circle. An unmeasured bin **hatches**. Everywhere else anullcan simply be dropped, but for bars a height of zero and a bar never drawn are the same picture — this isnull ≠ zeroat its hardest case.bin.noteprints a second axis reading under the first, replacing the timezone toggle a caller would otherwise build; a toggle hides half the truth and is missing from every screenshot. Also:errorstates on every chart that fetches —TimeSeries,Sparkline,MetricTable,NetworkGraph,Distribution— resolved throughresolveDataStateso the precedence is the array indata-state-modelrather than five hand-rolled ladders. In a dashboard "the fetch failed" and "there is no data" are different statements.DeltaandSeriesTableget none: neither has a fetch lifecycle.StatItemgainstone, carried by three signals rather than a hue alone — the rail, the value colour, and ansr-onlyjudgement. - Addedminor
TimeSeriescan plot more than one metric, and it finally draws an x axis.comparetakes further series —{ points, label, unit }— drawn against the same axes. It is purely additive:pointsis still the required single-series prop it always was, so nothing that already calls this component changes. Each line gets a--chart-Nhue **and** a distinct dash pattern, and the new legend swatch repeats the dash, so two lines stay two lines in a greyscale print and to a red-green colour-blind reader. Five series are drawn (the size of the palette); any beyond that stay in the data table and the legend says how many. All series share one y domain, and that is not configurable. A second y axis lets an author slide two unrelated metrics until they appear to cross wherever the argument needs them to; a series two orders of magnitude smaller will render as flat here, which is the true statement about it. The x axis is a baseline, a tick per labelled slot, and up to five HTML labels below the plot — HTML because SVG text scales with theviewBox, and at a 320 viewport this plot is 288px wide against a 900-unit box, sotext-xsinside it paints at 4px. Belowsmthe middle labels drop and the two ends stay. The crosshair readout now names and values every series at the cursor, from the onearia-live<output>that the arrow keys already fed. There is deliberately no second, hover-only tooltip to fall out of step with it.Components - Changedpatch
Six defects found by upgrading a real consumer, plus five found by reading the components closely enough to document them. **
Meteors' glow never painted.** It readvar(--color-meteor-glow)while itscssVarsdeclare--meteor-glow;--color-*is the Tailwind@themenamespace and onlycssVars.themepopulates it, so the wholebox-shadowwas invalid at computed-value time. **This was broken only for registry consumers** — our own docs site hand-declares the--color-form. **ArticleCardcropped 28% off every cover.**h-44is 176px; at a ~302px tile that is a 1.72:1 box against a 2.381:1 image. Nowaspect-[1000/420]— the ratio the card already declared on its<img>. It also gains arenderImageslot, because every Next.js consumer was re-patching the same line to usenext/imageand the design system cannot depend on it. **BorderBeamandStarsBackgroundhad noaria-hiddenat all** — six purely decorative nodes a screen reader walked. **CloudParticlesdefaultedbodyColortocurrentColor**, painting volumetric clouds in the inherited text colour. **NumberTickergainsnotation**, because six-figure metrics overflow a tile at 320px. Also:SheetComposeandDialogComposeeach mounted a second backdrop, so a composed dialog dimmed the page twice as much as the hand-composed tree the docs show;AccordiondroppedclassNameon the animated Panel;Tooltipaccepteddelayand discarded it;PopoverAnchorwas a second trigger. **useReducedMotionwas one frame late.** The canonicaluseState(false)plus effect returnsfalseon the first render, so every gated component painted one frame of exactly the motion the user turned off.useSyncExternalStorereads during render and closes that on client renders; on hydration the server cannot know the preference, which is what the stylesheet reset is for.Badgedrops'use client'— verified with a real server-component build. - Changedpatch
The fourteen
blocks/*re-export aliases now name the release they disappear in. They previously said "removal scheduled for one release cycle after the architecture PR lands", which is not a date anyone can plan around — so the aliases were, in practice, permanent. They are now@deprecated since 1.0.0 — removed in 2.0.0, and removal will land as a breaking change with a migration note.Components- @interlace/article-card
- @interlace/author-byline
- @interlace/code-window
- @interlace/empty-state
- @interlace/figure
- @interlace/hero
- @interlace/newsletter-form
- @interlace/page-header
- @interlace/prev-next-post
- @interlace/related-posts
- @interlace/section-header
- @interlace/share-buttons
- @interlace/sign-in-form
- @interlace/stat-card
- Changedpatch
Every component now describes itself, and 59 gained a file header.
descriptionsaid"@interlace/ui — accordion (shadcn-compatible)."for 128 of 137 items — the item's own name, restated. That field is whatshadcn addprints in your terminal, what the shadcn directory lists us under, what every card on the storefront shows, what<meta name="description">and OG carry, and what an agent reads to choose between two components. All of it said nothing. The sentence already existed in each component's header and was already being extracted — intoagent-index.json, the one surface an adopter never looks at. It is now derived once and published everywhere. Boilerplate descriptions went 128 → 0 and emptytopics72 → 19. What reaches your tree: the 59 components that gained a file header now carry that header in the installed copy. It is a comment block — no behaviour, no class names and no exports change. Everything else here is registry metadata that never leaves our side. - Changedpatch
The three entry animations in
styles/theme.cssnow run at 200ms with no delay, and the reduced-motion class list instyles/tokens.csscovers.animate-pulse..animate-fade-in-upwas 0.5s,.animate-slide-in-left0.5s behind a 0.3s delay, and.animate-scale-in0.4s behind a 0.2s delay — up to 800ms before the reader saw anything, against the 200ms entry ceilingMOTION_PHILOSOPHY.mdhas always set. All three are now0.2s ease-out both. The delays were the worse half: an entry animation is already laid out atopacity: 0, so a delay is time spent looking at nothing. Separately,.animate-pulse— the animationSkeletonrenders on every loading state — was missing from thetokens.cssreduced-motion list, along with.animate-meteorand.animate-meteor-effect. The universal clamp instyles/preflight.csswas already covering all three, so apps importingstyles/index.csswere never affected; apps that importtokens.cssandtheme.cssà la carte and skippreflight.csswere. All three are now listed. Both are held bypackages/ui/__tests__/motion-contract-lock.test.ts, which reads the ceiling out ofMOTION_PHILOSOPHY.mdrather than repeating it, and fails if any bareanimate-*utility inpackages/ui/srcis missing from the list. - Changedpatch
Two theme defects that only a browser could find, both of which read as "the theme system does not work".
useTheme()now keeps every instance in the SAME document in sync. Each instance owned private React state and only listened forstorageevents — which fire in OTHER documents — so a switcher in the nav updated itself and nothing else. A page with a switcher plus any second theme-aware component repainted the switcher and left the component on the previous theme, permanently. Harbor now declares its dark palette for[data-theme='harbor'] .darkand[data-theme='harbor'] [data-scheme='dark']as well as the same-element forms. The bare.darkblock is unscoped, so it means "Interlace dark": it re-declares every--interlace-*literal on whatever element carries it, and[data-theme='harbor'].darkneeds both on the same element. A<div class="dark">anywhere inside a Harbor page therefore repainted that subtree in the default brand.theme-contract-locknow requires the descendant forms from every registered theme.Components
v1.0.0
6 entries- Breakingmajor
The design system takes a version.
@interlace/uiwas0.0.0with no changelog, no release tag and no per-component version, while ds.interlace.tools had already been serving installable copies of every component. 1.0.0 is the first release with a written contract behind it: semver, per-component versions derived from git, a public changelog, and a documented definition of what "breaking" means for source you copied. - Addedminor
The visualization layer ships: a token-driven chart set built on the
VISUALIZATION_PHILOSOPHYrow-first doctrine —MetricTableas the centrepiece,TimeSeriesas the thing a row is promoted into, plus a network graph, sparklines and deltas. Every chart carries the DS-wideloadingskeleton opt-in. - Addedminor
The component floor reaches every tier: form, overlay, navigation and display primitives were modelled to the portable 26-rule floor with skeletons, locked breakpoints, and AA contrast measured per composite rather than eyeballed.
Components- @interlace/input
- @interlace/textarea
- @interlace/select
- @interlace/checkbox
- @interlace/radio-group
- @interlace/switch
- @interlace/slider
- @interlace/number-field
- @interlace/form
- @interlace/label
- @interlace/dialog
- @interlace/alert-dialog
- @interlace/sheet
- @interlace/popover
- @interlace/tooltip
- @interlace/hover-card
- @interlace/dropdown-menu
- @interlace/context-menu
- @interlace/tabs
- @interlace/accordion
- @interlace/collapsible
- @interlace/breadcrumb
- @interlace/pagination
- @interlace/toast
- @interlace/card
- @interlace/badge
- @interlace/avatar
- @interlace/alert
- @interlace/separator
- @interlace/progress
- @interlace/skeleton
- Addedminor
The five-layer architecture lands: primitives → patterns → charts → templates, with
SectionBoundaryas the composition seam and thirteen full-page templates installable as registry blocks.Components- @interlace/section-boundary
- @interlace/article-template
- @interlace/auth-template
- @interlace/author-template
- @interlace/blog-home-template
- @interlace/dashboard-template
- @interlace/docs-page-template
- @interlace/error-template
- @interlace/landing-template
- @interlace/registry-item-template
- @interlace/scorecard-template
- @interlace/settings-template
- @interlace/stats-template
- @interlace/tag-template
- Addedminor
The brand layer is forkable: the whole palette moved from Tailwind violet to the burnt-orange/green Interlace palette inside
@layer interlace.brandwith zero component edits, and the CSS baseline ships as one installablethemeitem — tokens, foundation, preflight (WCAG 2.2 SC 2.4.13 focus ring and the[data-min-viewport]contract), the shadcn↔fumadocs bridge, and the brand palette.Components - Addedminor
Day-one install surfaces: three starter bundles that pull a curated set of items in one command, and the two library utilities every primitive needs.