Changelog
Everything notable that changes in the Dovetail design system (system/), newest first. Versions follow Semantic Versioning as applied to a design system in docs/changelog.md: a renamed or removed token, component, prop or default is a breaking change.
<!-- Contributors: don't edit this file directly. Add an entry to changes/ with your pull request (npm run change -- <slug>); releases compile the entries into a new section here. Unreleased entries can be previewed with npm run changelog. -->
0.7.0 - 2026-09-30
Added
ChatBlockdraws a whole conversation from amessagesarray: it passestitle,subtitle,avatar,presence,onBackandactionstoChatHeader, computes runs (groupedfirst, middle, last), inserts aMessageDividerwheredaychanges and for{ kind: "event" }items, shows author names in group chats, and takestyping,quickReplies,composer,onRetry,height,labelandvariant="assistant"(aThinkingindicator and no presence).ChatBlock
A new Chat template (previews/ChatKit.html) composes two working prototypes from it: a customer support conversation with delivery statuses, a retry and canned replies, and an in-app assistant whose answer streams in word by word.
Checkbox,Radio,CheckboxGroupandRadioGrouptakelabelPosition="start", which puts the label on the left and the control at the right edge of the row, so the labels in a vertical group line up with the question above them instead of indenting. It matchesSwitch's prop of the same name. Set it on a group and every option follows. The default,"end", keeps the control first as before.CheckboxRadioCheckboxGroupRadioGroupMenuBlocklays out a restaurant's ordering page fromstore,fulfilmentandsections, with a sticky category nav that scrolls to each section and follows the reader, and per-dishquantities,onItemSelect,onItemAddandonQuantityChange;BasketBaris the floating "View basket" bar (count,total,onClick) that renders nothing while the basket is empty;OrderTrackingBlockshows theeta,status, amapslot with a drawn placeholder, acouriercard with named Call and Message buttons, and the order'slinesandsummary. The new Food ordering template (FoodKit) composes them into a working phone ordering flow, from menu to delivery.MenuBlockBasketBarOrderTrackingBlock- Four store blocks compose the commerce components into page sections:
ProductGridBlocksetsproductsin a grid ofcolumns(2, 3 or 4) that drops to two on a phone, with anactionbeside the header and anemptyState;ProductDetailBlockputs a gallery beside a buy box withvariants,quantity, a full-width add button gated bycanAddToCartorsoldOut, anddetailsin an accordion;CartBlocksetslinesbeside a stickysummarywithpromoandcheckoutAction, or an empty state; andCheckoutBlocklays out contact,address,deliveryOptionsandpaymentbeside the order, which folds into a "Show order summary" disclosure belowcollapseBelowand becomes a form when you passonSubmit. A new StoreKit template shows them working together, from browsing to an order confirmation.ProductGridBlockProductDetailBlockCartBlockCheckoutBlock
Fixed
CheckboxGroupandRadioGroupnow name theirrole="group"/role="radiogroup"element with theirlabeland describe it with theirhintorerror, so a screen reader announces the question before the options. Before, the group had no accessible name. To make this work,Fieldtakes two new optional props,labelIdandmessageId, which set the ids of its label and its hint or error line. Use them to wire any custom group the same way.CheckboxGroupRadioGroupField
0.6.0 - 2026-09-30
Added
- A chat family builds a conversation from props:
ChatHeader(title, subtitle, avatar,presence,onBack,actions),MessageList(a namedrole="log"that stays pinned to the newest message and offers a "New messages" jump when the reader has scrolled up) withMessageDivider,MessageBubble(from,time,statuswithonRetry, groupauthor, andgroupedrun shapes),Composer(controlled; Enter sends, Shift+Enter adds a line, grows tomaxRows, optionalonAttach),TypingIndicator(name) andQuickReplies(options,onSelect). Sent bubbles use the action surface, so they follow the ink-or-brand choice in Configure; the new--dt-bubble-*,--dt-presence-*,--dt-chat-*,--dt-typing-*and--dt-quick-replies-*tokens tune the rest.ChatHeaderMessageListMessageDividerMessageBubbleComposerTypingIndicatorQuickReplies--dt-bubble-sent-bg--dt-bubble-sent-fg--dt-bubble-received-bg--dt-bubble-received-fg--dt-bubble-meta-fg--dt-bubble-author-fg--dt-bubble-read-fg--dt-bubble-failed-fg--dt-bubble-radius--dt-bubble-radius-tight--dt-bubble-padding--dt-bubble-gap--dt-bubble-run-gap--dt-presence-online--dt-presence-away--dt-presence-offline--dt-presence-size--dt-chat-surface--dt-chat-border--dt-chat-divider-fg--dt-typing-beat--dt-typing-dot--dt-typing-dot-size--dt-quick-replies-gap--dt-quick-replies-border - Six cart and checkout components join the commerce family, all presentational and controlled: they show what they are given and call back, with no cart state, totals arithmetic, payment processing or network.
CartLineshows an item withimage,details,price/compareAt, an optionallineTotalandnote, and aQuantityStepperand Remove button named after the item (onQuantityChange,onRemove,maxQuantity), or "Qty 2" whenreadOnly, atsize"sm"or"md".OrderSummarylays outlines(withkind"discount"shown as a negative, and"muted") and atotalas a description list, with an optionalfreeShippingProgressbar and afooter.PromoCodeis a collapsible code field whoseonApplyfires on Apply or Enter, witherror,loadingand anappliedchip removed byonRemove.AddressFieldsandPaymentFieldsare fieldsets with the standardautoCompletetokens;PaymentFieldsformats card numbers (4-6-5 for Amex) andMM / YYas the user types and shows a textbrandbadge, and its guide directs real payments to the provider's hosted fields.OrderStatusis an ordered-list timeline witharia-current="step", astatusof"active","delayed"or"cancelled", and anorientation="horizontal"that turns vertical when its box is too narrow. Their colours and sizes are the new--dt-cart-*,--dt-summary-*,--dt-promo-*and--dt-order-status-*tokens.CartLineOrderSummaryPromoCodeAddressFieldsPaymentFieldsOrderStatus--dt-cart-line-divider--dt-cart-thumb-bg--dt-cart-thumb-border--dt-cart-thumb-fg--dt-cart-detail-color--dt-cart-note-color--dt-cart-thumb-radius--dt-cart-thumb-size-sm--dt-cart-thumb-size-md--dt-summary-bg--dt-summary-border--dt-summary-divider--dt-summary-label-color--dt-summary-value-color--dt-summary-muted-color--dt-summary-hint-color--dt-summary-discount-color--dt-summary-total-color--dt-summary-radius--dt-summary-padding--dt-promo-chip-bg--dt-promo-chip-fg--dt-promo-chip-border--dt-promo-chip-icon--dt-promo-chip-description--dt-order-status-complete--dt-order-status-on-complete--dt-order-status-current--dt-order-status-upcoming--dt-order-status-line--dt-order-status-delayed--dt-order-status-on-delayed--dt-order-status-delayed-text--dt-order-status-cancelled--dt-order-status-on-cancelled--dt-order-status-cancelled-text--dt-order-status-marker-size--dt-order-status-line-width--dt-order-status-step-min-width - Five food-ordering components join the commerce family.
StoreHeadertops a store page with a coverimage, alogo, thename, arating,metafacts, adeliveryTimeand adeliveryFee(0 reads "Free delivery"), and astatusthat dims a closed store and says so.FulfilmentToggleis a full-width delivery or pickup control with radio semantics and arrow keys, takingvalue,onChange, a requiredlabeland optionaloptionswith adetailline.MenuSectiongroups dishes under atitlewith an anchorid, as alayout="list"or a two-uplayout="grid".MenuItemshows a dish'sname,description,price,imageand dietarytags, withsoldOut, aquantitybadge and stepper (onQuantityChange), an add button (onAdd), and a whole-rowonSelectthat stays a separate focus target from add.ModifierGroupoffers a dish's options inmode="single"or"multiple"with price deltas,required,min/maxlimits that disable the rest and say why, and an announcederror. Their tokens are the new--dt-store-*,--dt-fulfilment-*,--dt-menu-*,--dt-modifier-*and--dt-dietary-*.StoreHeaderFulfilmentToggleMenuSectionMenuItemModifierGroup--dt-store-cover-bg--dt-store-cover-radius--dt-store-cover-max-height--dt-store-logo-size--dt-store-logo-radius--dt-store-logo-bg--dt-store-logo-ring--dt-store-meta-color--dt-store-open-color--dt-store-closed-color--dt-store-closed-scrim--dt-fulfilment-track-bg--dt-fulfilment-thumb-bg--dt-fulfilment-thumb-shadow--dt-fulfilment-fg--dt-fulfilment-fg-selected--dt-fulfilment-detail-color--dt-fulfilment-detail-color-selected--dt-fulfilment-inset--dt-fulfilment-radius--dt-fulfilment-thumb-radius--dt-menu-item-gap--dt-menu-item-padding--dt-menu-item-divider--dt-menu-item-bg-hover--dt-menu-item-card-bg--dt-menu-item-card-border--dt-menu-item-description-color--dt-menu-item-soldout-color--dt-menu-thumb-size--dt-menu-thumb-radius--dt-menu-thumb-bg--dt-menu-grid-min--dt-menu-section-gap--dt-modifier-row-min-height--dt-modifier-divider--dt-modifier-control-size--dt-modifier-control-border--dt-modifier-control-bg--dt-modifier-control-accent--dt-modifier-control-mark--dt-modifier-delta-color--dt-modifier-hint-color--dt-modifier-pill-bg--dt-modifier-pill-fg--dt-modifier-error-color--dt-modifier-error-pill-bg--dt-dietary-vegetarian-bg--dt-dietary-vegetarian-fg--dt-dietary-vegan-bg--dt-dietary-vegan-fg--dt-dietary-spicy-bg--dt-dietary-spicy-fg--dt-dietary-gluten-free-bg--dt-dietary-gluten-free-fg--dt-dietary-popular-bg--dt-dietary-popular-fg--dt-dietary-new-bg--dt-dietary-new-fg--dt-dietary-default-bg--dt-dietary-default-fg - A new commerce family starts with three primitives.
Priceformatsamountfor itscurrencyandlocalewithIntl.NumberFormat, strikes through a highercompareAtprice and announces "Was …, now …", and takes aunit, asizeand afreeLabel.Ratingshows avaluein half stars with an optional reviewcount, or, givenonChangeand a requiredlabel, becomes a keyboard-operable radio group of stars.QuantityStepperis a − value + control with a typeablerole="spinbutton"field,min,maxandstep, and anonRemovethat turns the minus button into a remove button at the minimum. Their colours are the new--dt-price-*and--dt-rating-*tokens.PriceRatingQuantityStepper--dt-price-color--dt-price-sale-color--dt-price-compare-color--dt-price-unit-color--dt-rating-color--dt-rating-empty-color--dt-rating-count-color - The commerce family gains three product components.
ProductCardshows a product'simage,name,price(withcompareAt,currencyandlocale),rating,badge,subtitleand colourswatchesin averticalorhorizontallayout; withhrefthe name is its one link, stretched over the card, while anactionor theonQuickAddbutton stays separately clickable, andsoldOutwashes out the photo and disables both.ProductGalleryshowsimageswith previous and next buttons, thumbnails at thebottom, on theleft(moving below when the gallery is narrower thancollapseBelow) ornone, a counter, keyboard and swipe navigation, and a controlledvalue/onChangeor an uncontrolleddefaultIndex.VariantPickerchooses one option aschips, colourswatchesor aselect, as a radio group whose arrow keys skipdisabledoptions, with the chosen label in the legend (showSelected). Their colours are the new--dt-product-*,--dt-swatch-*,--dt-variant-*and--dt-gallery-*tokens.ProductCardProductGalleryVariantPicker--dt-product-subtitle-color--dt-product-soldout-overlay--dt-swatch-border--dt-swatch-ring--dt-variant-bg--dt-variant-bg-hover--dt-variant-fg--dt-variant-border--dt-variant-selected-bg--dt-variant-selected-fg--dt-variant-selected-border--dt-variant-unavailable-fg--dt-variant-unavailable-border--dt-variant-note-color--dt-gallery-control-bg--dt-gallery-control-bg-hover--dt-gallery-control-fg--dt-gallery-thumb-border--dt-gallery-thumb-selected-border
Changed
- The install instructions everywhere (README, docs overview, Download page, home hero) read
npm install @dovetail-ds/react react react-dom, with a line on why:react-domis not a peer of the package, since nothing in it importsreact-dom, but an app needs it to render, and npm records an auto-installed peer only in the lockfile, so a fresh project could end up with neitherreactnorreact-dominpackage.json.
The dovetail-setup skill now checks for exactly that and adds what is missing, and it handles a folder with no app yet: it offers to scaffold Vite + React + TypeScript (or Next.js) around the package before asking the theme questions, from references/scaffold.md.
0.5.0 - 2026-09-30
Added
- The package ships a Claude Code skill,
skills/dovetail-setup, that sets Dovetail up in a project. It interviews you about brand colour, buttons, type, corners, spacing and surfaces, or imports atheme-custom.cssdownloaded from Configure. Then it writes the theme, wiresfonts.css,styles.cssand the theme into the app's root in the right order, and reports contrast failures with exact fixes. Copy it into.claude/skills/to use it; the README shows how.
The skill runs Configure's own code from the installed package (dist/configure/core.js, which is not public API), so a theme built from answers is byte-for-byte the file Configure's "Download theme.css" gives for the same choices. Its script also checks any theme file: anything that isn't a token declaration is an error, and a token this version doesn't know is a warning.
0.4.0 - 2026-09-30
Added
require("@dovetail-ds/react")works on Node 22.12 and later: the exports map uses thedefaultcondition, so a Node that canrequire()an ES module loads the package without a separate CommonJS build. Older Node versions still needimport.
Fixed
Dialogbehaves as a modal: it is named by itstitle(aria-labelledby) and described by itsdescription, focus moves into it when it opens, Tab and Shift+Tab stay inside, the page behind stops scrolling, and focus returns to the opener on close. A newlabelprop names a dialog with no visible title, andstyleand otherdivattributes pass through to the panel.Drawergains the same focus trap and scroll lock, and is named by its title whatever type it is.DialogDrawer
Before this, a screen reader announced the dialog with no name, focus stayed on the opener, and Tab after the last button left the modal. Consumer audits of 0.2.0 found all three.
Tabsskips disabled tabs from the keyboard: ArrowLeft, ArrowRight, Home and End move only among enabled tabs, wrapping at the ends, andonChangeis never called with a disabled tab's id. Before this, ArrowRight from a tab next to a disabled one selected the disabled tab and showed its panel.Tabs
0.3.0 - 2026-09-30
Breaking changes
@dovetail-ds/react/styles.cssno longer loads Geist from Google Fonts. The font@importmoves to a new opt-in@dovetail-ds/react/fonts.css, so the stylesheet makes no third-party request, works offline and under a strict CSP, and leaves self-hosting up to you. (visual)--dt-font-family-sans--dt-font-family-mono
Without fonts.css or your own @font-face for "Geist" and "Geist Mono", text falls back to the system font. The docs site is unchanged.
Migration
To keep Geist from Google Fonts, import fonts.css before styles.css:
``js import "@dovetail-ds/react/fonts.css"; import "@dovetail-ds/react/styles.css"; ``
To self-host, leave fonts.css out and serve the fonts under the same family names (for example with next/font/local or @font-face), or point --dt-font-family-sans and --dt-font-family-mono at your own families. Afterwards, check that headings and body text render in Geist rather than the system font.
Changed
reactis the package's only peer dependency:react-domis no longer required (nothing in the package imports it), and the published manifest has noenginesfield, so installs on any Node version stop warning. The npm description now describes the package rather than the repository.
Fixed
- The type declarations work with React 19: every
.d.tsusesReact.JSXinstead of the globalJSXnamespace, which@types/react19 removed. With React 19 types, every component's declaration previously failed with "Cannot find namespace 'JSX'". The package check now type-checks against@types/react18 and 19. - The package works in React Server Components and the Next.js App Router: interactive components start with
"use client", while layout and type components (Section,Stack,Heading,Textand others) stay server components.Navbarno longer crashes when it collapses on a narrow screen, andNavbarandSheetrender the same markup on the server and on the first client pass, so hydration matches.NavbarSheet
Navbar rendered its mobile menu with Drawer without importing it, which only worked on the docs site, where every component shares one scope. Both components now read matchMedia through useSyncExternalStore, with a server snapshot of false, instead of in their initial state. The package build now fails on any component that renders another one without importing it.
0.2.0 - 2026-09-29
Breaking changes
- Dovetail's default look is monochrome and round: buttons, links, selection and focus are ink instead of the primary colour, secondary buttons are soft grey fills, buttons are pills, cards are bordered and flat, the greys have no hue, and the default primary is a warm orange used for accents. (visual)
ButtonIconButtonCardTabsLink--dt-surface-action--dt-text-link--dt-surface-selected--dt-border-selected--dt-focus-ring-color--dt-surface-action-secondary--dt-color-neutral-*--dt-color-primary-*--dt-radius-control--dt-radius-container--dt-radius-overlay--dt-button-radius--dt-card-elevation
In detail:
- Colour.
--dt-surface-action,--dt-text-link,--dt-surface-selected,--dt-border-selectedand--dt-focus-ring-colorpoint at the neutral ramp (ink on light, near-white on dark). Each has a new-brandtwin holding the old primary values.--dt-surface-action-secondaryis a soft grey fill with no border. The neutral ramp is pure grey (chroma 0); the primary ramp is a warm orange (500 is#eb6834) instead of blue. - Shape.
--dt-radius-control6→8px,--dt-radius-container8→16px,--dt-radius-overlay12→24px,--dt-radius-media8→12px.--dt-button-radiusreads--dt-radius-pill. - Depth. Cards are bordered and flat (
--dt-card-elevationis--dt-elevation-0). Shadows are softer and wider, with a hairline ring on the floating levels. - Type. Display and heading roles are weight 500 with tighter tracking (new
--dt-tracking-tightest, −0.04em). Body text uses tabular figures. - Motion and glass.
--dt-easing-standardiscubic-bezier(0.2, 0.8, 0.2, 1); glass saturates 1.6×.
Migration
To keep brand-coloured buttons, links, selection and focus, point each role at its -brand twin (or choose Buttons and links: Brand in Configure):
``css :root, .dark { --dt-surface-action: var(--dt-surface-action-brand); --dt-surface-action-hover: var(--dt-surface-action-brand-hover); --dt-surface-action-active: var(--dt-surface-action-brand-active); --dt-text-on-action: var(--dt-text-on-action-brand); --dt-text-link: var(--dt-text-link-brand); --dt-text-link-hover: var(--dt-text-link-brand-hover); --dt-surface-selected: var(--dt-surface-selected-brand); --dt-surface-selected-hover: var(--dt-surface-selected-brand-hover); --dt-text-on-selected: var(--dt-text-on-selected-brand); --dt-border-selected: var(--dt-border-selected-brand); --dt-focus-ring-color: var(--dt-focus-ring-color-brand); } ``
To keep the previous shapes, set --dt-radius-control: 6px; --dt-radius-container: 8px; --dt-radius-overlay: 12px; --dt-radius-media: 8px; --dt-button-radius: var(--dt-radius-control); and --dt-card-elevation: var(--dt-elevation-1);. A theme that already sets its own primary ramp keeps its hue; only the controls change colour. Check screens with a primary button on a brand fill, which is now ink on colour.
Added
data-surface="brand-muted"sets a region, or the whole page onhtmlorbody, to the brand's own tint instead of white and grey:--dt-surface-basebecomes--dt-surface-brand-muted(the primary's 050 step, 950 in dark) and--dt-surface-subtlesteps a little toward ink, so a band inside still reads against the page. Text keeps its ordinary roles, and a region that is also.darkresolves the dark tint.Section tone="brand-muted"remains for a band that also re-colours its text from the brand. Nothing changes unless the attribute is set.SectionCardcan sit on a picture:backgroundfills it with an image andbackgroundVideowith a muted looping video. It sets its content at the bottom over ascrim(gradient,solidornone), andonMediasets the text in near-white, pure white or a brand-tinted title.Card--dt-text-on-scrim-strong--dt-text-on-scrim-brand--dt-size-media-min--dt-card-media-fg--dt-card-media-fg-secondary--dt-card-media-fg-strong--dt-card-media-fg-brand--dt-card-media-border--dt-card-media-min-height
A pictured card is dark in both colour modes (it scopes .dark, so buttons in its footer read light on dark) and holds at least --dt-card-media-min-height. The video doesn't autoplay for someone who prefers reduced motion; they see background as its still. New semantic tokens --dt-text-on-scrim-strong (pure white) and --dt-text-on-scrim-brand (a light brand step) are available for any text over imagery.
- Four layout layers say how closely the two things either side of a gap belong together:
related,group,blockandsection, each with a stack and an inline gap (--dt-layout-stack-*,--dt-layout-inline-*).StackandInlinetake them aslayer, which wins overgap.StackInline--dt-layout-stack-related--dt-layout-stack-group--dt-layout-stack-block--dt-layout-stack-section--dt-layout-inline-related--dt-layout-inline-group--dt-layout-inline-block--dt-layout-inline-section
A layout character moves the layers as one: tight for a technical screen, balanced (the default), or open for room to breathe, where what is related stays close and the layers move apart. Set it on any region with data-layout, or with the new spacing prop on Stack and Inline; the layer tokens are re-declared under each value, so a region set inside a page of another character returns to its own. No existing token or prop changes.
The Space foundation page is now Layout (foundations/layout.html, with the old address forwarding), and gains a Layers card.
- The layout has two more groups, both moved by the layout character (
tight,balanced,open). Text is the gaps between the items of a block of text:--dt-layout-text-eyebrow(an eyebrow and its heading),-subcopy(a heading and its lead) and-paragraph. Modules is the room a module takes:--dt-layout-module-paddingabove and below its content, and--dt-layout-module-gapbetween its own parts. Atbalancedthey are--dt-space-stack-sm,-md,--dt-space-sectionand--dt-space-stack-xl, so nothing changes until the layout does, and a context still moves them.StackInlineSectionBlockHeaderHeroBlockFeatureGridBlockStatsBlockTestimonialBlockFaqBlockSocialPost--dt-layout-text-eyebrow--dt-layout-text-subcopy--dt-layout-text-paragraph--dt-layout-module-padding--dt-layout-module-gap--dt-layout-scale
Stack takes eyebrow, subcopy and paragraph as its layer, and Stack and Inline multiply every layer by --dt-layout-scale, 1 on a page. Section's default padding, BlockHeader's gaps and the header-to-content gap in the hero, feature grid, stats, testimonial and FAQ blocks read them.
SocialPost is built on Stack and Inline, with its margin and gaps as layout layers drawn at --dt-social-scale (2.5), so a post follows the page's layout, and takes spacing to set its own. Balanced looks as before.
- A new Blocks family of page sections that stack into landing pages:
HeroBlock,FeatureGridBlock,SplitBlock,StatsBlock,TestimonialBlock,FaqBlockandCtaBlock, plusBlockHeaderfor starting a custom block in the same rhythm.HeroBlockFeatureGridBlockSplitBlockStatsBlockTestimonialBlockFaqBlockCtaBlockBlockHeader
Each block is a Section with its layout decided and its content as props, and takes Section's tone, dark, texture, spacing and width, so a page is a list of blocks alternating tone. Layouts collapse to one column on a phone without media queries. A new "Landing page from blocks" template shows a full page built only from them.
- Configure's Fill has a Quiet option.
--dt-surface-brandbecomes the palest tint of the primary (--dt-surface-brand-muted, its 050 step, 950 in dark) and--dt-text-on-brandthe text that belongs on it, so every band, block and social post on the brand tone goes quiet at once. The-mutedtones stay as they are, for a single band. The choice is declared under.darktoo, so a dark band inside a light page resolves its own tint; Gradient and Duotone now do the same.SectionSocialPost - New
Sheetcomponent: a modal panel that rises from the bottom of a phone, inset from its edges, and opens as a centred dialog on a wide screen. It has a sticky bar with close (or back, viaonBack) and anactionslot, a bigtitlethat shrinks into the bar on scroll,actionschips or afooterpinned to the bottom, and drag-down-to-close and drag-right-to-go-back on touch.Sheet--dt-sheet-inset--dt-sheet-top-gap--dt-sheet-padding--dt-sheet-button-size--dt-sheet-button-bg--dt-sheet-shadow
It shares the overlay tokens with Dialog and adds --dt-sheet-* for its inset, top gap, padding, round buttons and shadow. Focus moves in on open, is trapped while open and returns on close; Escape and the scrim call onClose with the reason; reduced motion skips the open and close animations.
- New
SocialPostcomponent for Instagram stories (9:16) and grid posts (4:5 or 1:1). It draws at native pixel size (1080 wide) and scales to fit, in ten layouts that share one frame and editorial type scale:headline,quote,stat,listandannouncementon atone, andcover,split,framed,cardandposteraround animage.SocialPost--dt-dim-artboard-width--dt-dim-artboard-square--dt-dim-artboard-portrait--dt-dim-artboard-story--dt-size-artboard-width--dt-size-artboard-square--dt-size-artboard-portrait--dt-size-artboard-story--dt-text-artboard-display-size--dt-text-artboard-title-size--dt-text-artboard-body-size--dt-text-artboard-meta-size--dt-social-width--dt-social-padding--dt-social-display-size--dt-social-bg--dt-social-fg--dt-social-on-image
Its sizes come from new artboard tokens (--dt-size-artboard-*, --dt-text-artboard-*), drawn for a 1080px canvas and never retuned by a context; its margin and gaps are layout layers drawn at the artboard's scale (Stack and Inline throughout), so a post follows the page's layout, and its colours alias the ordinary roles, so a theme rebrands every post. A new "Social templates" template shows all ten as stories, a 4:5 grid and squares.
- The marketing and dashboard templates draw line icons in navigation, buttons, feature cards and stat tiles. They also carry photo, video and illustration placeholders: fill a slot by dropping a file on it or by uploading in Configure's Media tab. (visual)
ImageVideoCover
The icons come from system/templates/_support/template-icons.js, demo chrome drawn on the documented 24px convention. They follow Configure's icon stroke and size controls. The template switcher's Base theme now shows the shipped defaults instead of forcing the old blue ramp and radii. The site previews are compiled from system/kits/ by npm run build, so the two no longer drift.
Changed
- Accordion's disclosure icon is a plus that turns into a minus as a panel opens, in place of the chevron. Panels ease open and shut, with their height and opacity following
--dt-motion-emphasis, so they respect reduced motion. A closed panel stays in the document but is hidden from the tab order and assistive technology. (visual)Accordion
Fixed
- Button's loading spinner no longer logs an SVG error: its
widthattribute held a CSS variable, which browsers reject. The spinner is still sized by--dt-size-icon-sm.Button CardwithbackgroundVideostarts its video from an effect rather than anautoPlayattribute, so the server and the browser render the same markup and React no longer warns about a mismatch for readers who prefer reduced motion. The video still plays unprompted (it is muted and inline), and stays on its poster when reduced motion is on.Card- The preview cards and the templates are responsive on a phone. None of them said
width=device-width, so a phone laid every card out at 980px and shrank it to fit, and no media query in any of them ever ran; they only looked responsive in a desktop window dragged narrow. Every card now carries the viewport meta. (visual)
Twelve colour and elevation cards, the type and space scales, the wordmark, the layers card, the settings page template and the theme configurator card were wider than a 390px screen. Their swatch rows become a grid, the settings page's sidebar becomes a strip above the page, the configurator's panel stacks over its preview, and the type scale sets each sample under its label. The docs pages also break a long path in a paragraph rather than widening the page. npm run check now loads every card and page as a 390px phone and fails on a missing viewport meta or content wider than the screen.
- Sixteen components' type declarations no longer clash with the native HTML attribute of the same name.
title(a richReactNode),content,role,sizeandonChange(with the component's own signature) are left out of the inherited HTML props, so the declarations type-check under TypeScript with@types/react18, and<Tabs onChange={(id) => …}>or<Input size="lg">no longer reads as an error. No runtime change.CalloutCoverMediaQuoteCardEmptyStateAlertBannerDrawerToastTooltipInputSelectAppShellPaginationTabs SocialPostheadings keep the post's own colour on a page that stylesh2, so abrandor photo post no longer shows dark text where it should be white. Body copy and thelistlayout ignore a hostmax-widthonpandol. If the--dt-social-*tokens haven't loaded, the artboard falls back to its native size (1080 × 1920, 1350 or 1080) instead of drawing blank or oversized. (visual)SocialPost
On the docs site, links to preview cards now carry a stamp of the card and of the component bundle. A card opened after a release therefore loads the new bundle instead of a cached one that predates its component. This is what left the Social templates card blank.
- Every component now renders on the server with
renderToString, so a Next.js-style page no longer logs React warnings.SocialPostwas the only one that didn't: its scale measurement useduseLayoutEffect, which React warns about during a server render. It now falls back touseEffectwhere there is nodocument, and behaves as before in the browser. The other 75 components already rendered cleanly.node tools/check/ssr.mjsrenders all 76 and fails on any throw,console.errororconsole.warn.SocialPost
0.1.0 - 2026-09-28
The baseline: the first tracked version of Dovetail, recording what the system ships as of this release. Later releases list changes against it.
Breaking changes
- The
accentramp and roles are nowprimary:--dt-color-accent-*became--dt-color-primary-*, andtone="accent"becametone="primary"on Badge, Progress and Link. Listed because exports made before the rename use the old names.
Migration
Find and replace --dt-color-accent- with --dt-color-primary-, and tone="accent" with tone="primary". --dt-surface-selected and its hover now point at the primary ramp, so selected states follow the brand colour.
Added
- Tokens. Three tiers: primitive, semantic and component. Every component colour alias is repeated under
.dark, so scoped dark bands work. The set covers:- OKLCH colour ramps: primary, secondary, neutral and the feedback hues;
- a type scale with display, body, secondary and mono families;
- space, size, shape, elevation and blur, motion, and brand fills and textures;
- layered surfaces for glass, image scrims and blur (
--dt-surface-glass*,--dt-scrim-*,--dt-backdrop-glass).
- Themes and contexts. A base dark theme and a custom theme. Product, marketing and social contexts. Density, editorial and mono variations.
- 66 components in one bundle (
system/components/bundle.js, namespaceBeamMobileDesignSystem_e33121), each with.d.tstypes and usage docs:- Actions: Button, ButtonGroup, IconButton, Link
- Content: Accordion, AspectRatio, BlockRenderer, Callout, Cover, Figure, Image, Media, Prose, Quote, Video
- Display: Avatar, AvatarGroup, Badge, Card, Code, EmptyState, List, Skeleton, Stat, Table, Tag
- Feedback: Alert, Banner, Dialog, Drawer, Popover, Progress, Spinner, Thinking, Toast, ToastRegion, Tooltip
- Forms: Checkbox, CheckboxGroup, Combobox, Field, Input, Radio, RadioGroup, Select, Slider, Switch, Textarea
- Navigation: AppShell, BottomNav, Breadcrumbs, Navbar, Pagination, Sidebar, Stepper, TabPanel, Tabs
- Primitives: Divider, Grid, Inline, Section, Spacer, Stack, VisuallyHidden
- Typography: Heading, Text
- Thinking, an assistant's loading states: connecting, listening, thinking, searching and speaking.
- Nine shapes: five fluid (blob, orb, tile, dots, bars) and four textural (matrix, ascii, particles, sequence).
- Inline or as a voice overlay, on a dark or light screen.
tone,colors,speed,intensityand a livelevel.
- Brand controls in Configure:
- an exact brand hex anchored in its ramp, with editable steps and WCAG checks that explain themselves;
- a secondary brand colour and a secondary type family;
- 4 to 10 steps per ramp;
- headline and wordmark colour roles.
- Templates: settings page, dashboard and marketing kits.