Vintage Frames

Install

npm install vintage-frames
import 'vintage-frames' // registers every <vf-*> element

Or import individual elements:

import 'vintage-frames/vf-button.js'
import 'vintage-frames/vf-checkbox.js'

vf-desktop

The gray desktop: a 50% dither, plus window management. Pointerdown or keyboard focus in a slotted vf-window brings it to front and marks it active, clearing active on the others. DOM order follows the stacking order, synced in a task after the press ends, so a click on a control in a background window still reaches it. Utility windows float on their own tier above document windows, outside the single-active rule; a slotted vf-menu-bar (or free-standing vf-menu) sits above both, so its dropped menus cover palettes too. clearActive() deactivates the whole document tier — zero active windows is a legal state — until a press, keyboard focus, or a newly slotted window reactivates it. A press on the desktop's own dither changes nothing: the page decides which presses mean the Finder. Every change of holder fires vf-activate.

Sized in system px, never from your stylesheet — the component's own inline size wins. Set the numbers directly or let fitWithin derive them, then position the desktop with your own CSS. Windows and icons inside place themselves with their own top/left.

Anatomy
KindNameNotes
Propertywidth, heightThe screen raster, in system px (defaults 512×342). The host box is the screen plus 2×bezel per axis
PropertybezelThe black screen surround, in system px (default 0), added on every side. Flow, window coordinates and the drag clip belong to the screen; the corner mask sits on all four of its corners, so a menu bar inside needs no rounded
PropertypatternThe desktop pattern: a library name (default gray-50, the dither) or sixteen hex digits stating a custom 8×8 pattern — see vf-container for the library. An unrecognized value keeps the dither; --vf-desktop-pattern wins over it
PropertyactiveWindowRead-only: the active document-tier window, or null while the tier is deactivated or empty. Utility windows are never the holder
MethodfitWithin(w, h)Sets and returns the largest whole raster whose host box, bezel included, fits a CSS-px bound. Call on resize and onScaleChange to fill the viewport
MethodclearActive()Deactivates the document tier. A press or focus in a document window, or a newly slotted one, reactivates; utility windows keep their active
MethodplacementAt(x, y)A viewport point (CSS px) as { left, top } on the screen, whole system px on the placement lattice, bezel excluded — the pair to write onto a child dropped here
Eventvf-activate{ window } — the new holder, null on deactivation. Once per change of holder; re-asserting the current one is silent
Slot(default)Menu bar, windows, an icon field, anything. A dragged vf-icon anywhere inside draws its outline on the screen, above the menu bar and clipped at the raster's edge
PartdesktopThe dithered surface
Token--vf-desktopBase color under the tile (#808080)
Token--vf-desktop-patternThe whole background-image tile

vf-window

The window shell. The 1992 Macintosh Human Interface Guidelines names five standard windows; each is a one-line recipe.

The five HIG archetypes as recipes
ArchetypeRecipe
Document window<vf-window closable zoomable movable resizable scrollbars="both">
Movable modal dialog box<vf-dialog heading="…">
Modal dialog box<vf-dialog frame="plain">
Modeless dialog box<vf-window closable movable>
Utility (floating) window<vf-window variant="utility" movable>
Properties
PropertyTypeDefaultNotes
widthnumberWhole system px. Declare it, with height; undeclared, the window falls back to plain block layout and warns once in the console
heightnumberWhole system px. Content taller than the box clips at the frame; scrollbars reaches the rest. Unset, the body sizes to its content and warns once in the console
headingstring''Title text, centered on the stripes. Also qualifies the widgets' accessible names (Close Read Me)
activebooleantrueFrontmost. Inactive drops the stripes, stops drawing the widgets (they keep their tab stops), hollows the grow box and blanks every scroll rail inside; the title stays black. Managed by vf-desktop; outside one, nothing changes it
closablebooleantrueThe left close box
zoomablebooleanfalseThe right zoom box
movablebooleanfalseDrag the title bar to move; the drag writes top/left in system px
resizablebooleanfalseThe 15×15 grow box, bottom right; writes width/height within the size rect below and fires vf-resize
min-width, min-heightnumber80, 54Whole system px. The narrowest and shortest the grow box will make the window. Properties are minWidth / minHeight
max-width, max-heightnumberWhole system px. The widest and tallest. Equal to the min, an axis is locked and the grow box changes the other alone. Bounds the gesture only: a size authored outside the rect renders as declared. Properties are maxWidth / maxHeight
variant'utility'The windoid: 12px bar with a dot dither instead of racing stripes, 7×7 widgets, no title patch. Floats above document windows inside a vf-desktop
scrollbars'vertical' | 'horizontal' | 'both'Rails on the window's edge; content runs to the frame and the rails. A resizable window's grow box lands in the corner cell, reserved on a single-axis rail too; with a status strip the grow box sits in the strip and the rail runs edge to edge
header-heightnumberWhole system px, rule included: the header's height. Unset, the header is as tall as its content plus the rule. Property is headerHeight
placementAt(x, y)methodA viewport point (CSS px) as { left, top } in the content region, whole system px on the placement lattice — under scrollbars the scrolled plane, offset included. The pair to write onto a child dropped here
measure()methodRe-measures the built-in scroll area; a no-op without scrollbars. The area tracks its content, its slot and every placed child's top/left write by itself
Anatomy
KindNameNotes
Slot(default)Body content. Starts at the content region's corner, where a placed child's (0,0) is; the body adds no inset, so an inset is the content's own, such as a vf-stack pad
SlotheaderThe header: a band between the title bar and the body across the whole window, a white interior over a 1px rule. No inset, and a placed child's (0,0) is its corner. Under scrollbars the vertical rail begins below it. Takes no space until populated
SlotstatusThe bottom status strip: a 1px rule over a 15px white band, body-face text on its native line, the grow box flush in its right end. Takes no space until populated
RolegroupNamed by its heading
Eventvf-closeThe close box was clicked. The window does not remove itself — the consumer decides
Eventvf-zoomThe zoom box was clicked
Eventvf-resize{ width, height, commit }, system px — one per size the grow box writes, fired after the box is applied (a handler that measures reads the new layout), then commit: true as the gesture settles. Programmatic size writes fire nothing
Partsframe, title-bar, title, close-box, zoom-box, header, body, status-bar, grow-boxPlus viewport when scrollbars is set
Tokens--vf-titlebar-height, --vf-titlebar-height-utility, --vf-status-bar-height, --vf-title-inset, --vf-dots-pattern, --vf-shadow-offset18px / 12px bars, the 15px status strip, the title's clearance from the widgets, the windoid dither, the hard shadow

vf-dialog

The modal-dialog shell, over a native <dialog>: it opens in the top layer, traps focus and closes on Escape — and, with light-dismiss, on a click outside it. Both chromes are the modal double frame — 1px rule, 2px gap, 2px band, no shadow. Movable modal by default (the striped bar set into the top of the frame, no close box); frame="plain" is the modal dialog box (no bar, immovable). No backdrop dimming. top/left are viewport coordinates, and unset means centered. It opens with focus in its first text field, or on the default button; Return activates the default button from anywhere in the dialog.

Properties
PropertyTypeDefaultNotes
openbooleanfalseReflected, and kept in sync with the native dialog both directions
widthnumberWhole system px. Declare it, with height — an undeclared dialog reflows as it is dragged toward a viewport edge. Unset, it falls back to 260 and warns once in the console
heightnumberWhole system px; content taller than the box clips at the frame. Unset, the body sizes to its content and warns once in the console
headingstring''Title-bar text, and the accessible name. On frame="plain", which has no bar, it only names the dialog
labelstring''Accessible name for a dialog with no heading; wins over one when both are set. Falls back to 'Dialog'
closablebooleanfalseOpt into the close box. Ignored under frame="plain", which has no bar
light-dismissbooleanfalseA click outside the frame closes the dialog, with reason: 'outside'. Both the press and the release have to land outside: a press that starts on a control and slides off, or a title-bar drag, leaves it open. The click is consumed — nothing under the backdrop sees it. Property lightDismiss, reflected; read at the release, so it can be set on an open dialog
frame'plain'The bare double frame: no title bar, immovable. Also the shell an alert box is composed from (last example below). The content area starts 5px inside every edge; under the default bar it starts 20px below the top
Anatomy
KindNameNotes
Methodshow()Opens it (native showModal()), pinned onto the device-pixel grid
Methodclose()Closes it; fires vf-close with reason: 'close'
Eventvf-close{ reason: 'escape' | 'close' | 'outside' } — every close path routes through the native close event. 'outside' only under light-dismiss
KeyboardInitial focusOn open: a slotted control with autofocus, else the first text-entry control (vf-text-field, vf-number-field, vf-text-area, native text input/textarea), else the default button (vf-button variant="default")
KeyboardReturn / EnterActivates the default button from anywhere in the dialog, a focused Cancel included; Space presses the focused control. A link keeps its own Enter. In a text area Return inserts a newline and the keypad's Enter activates the button. Escape closes
Slots(default)The content. A child placed with top/left measures from the frame's inner edge, and flow content starts at the same corner
Partsframe, title-bar, title, close-box, body, contentTitle bar and close box only on the default chrome

An alert box is a recipe, not a component: the plain frame, a row vf-stack with 32×32 art, copy in the chrome face, and a vf-button-group, each placed with top/left. Set label — the plain frame has no title bar to take a name from.

vf-separator

A 1px black rule. Inside a menu panel it renders as the dimmed dotted rule. It has no width of its own: give it one.

Properties
NameTypeDefaultNotes
verticalbooleanfalse1px wide, auto height — give it a height context
--vf-separator-colorToken--vf-blackMenus set it to the dim gray
--vf-separator-styleTokensolidMenus set it to dotted
Role separator. No slots, parts or events.

vf-button

The push button: pixel-stepped corners, not rounded. Pressing inverts the face; Tab draws the dashed rule under the label.

Properties
PropertyTypeDefaultNotes
variant'default'The default button. The 3px ring sits 1px clear of the face and reserves 4px of margin; buttons sharing a row belong in a vf-button-group
disabledbooleanfalseDims the label only; the 1px border stays black (the default ring dims, its inner border does not)
type'button' | 'submit' | 'reset''button'Form-associated: submit calls requestSubmit(), reset calls reset()
name, valuestring''Standard — the submitting button's pair in the form data. A form-associated custom element can never be a form's event.submitter, so read the control as event.submitter.closest('vf-button')
Anatomy
KindNameNotes
Slot(default)The label
PartbuttonThe inner <button> — put a min-width here
EventsNone custom; native click suffices
Rolenative <button>Enter and Space activate
Tokens--vf-button-height (20px), --vf-button-ring-margin, --vf-button-flexThe last two are the hooks vf-button-group sets; standalone they are inert

vf-button-group

A row of buttons sized to the widest, faces aligned. A plain flex row aligns a default button's ring instead of its face — compare the two stages below.

Properties
NameTypeDefaultNotes
verticalbooleanfalseStack in a column instead of a row
naturalbooleanfalseLet each button keep its own content width
--vf-button-group-gapToken12pxGap between buttons
Slot: default (vf-button elements). No parts, no events. It shrink-wraps to its buttons; the parent positions it (justify-self: end for an action row). vf-dialog wraps its buttons slot in one.

vf-swatch

A color-swatch button: a white-inset color well sized in system pixels. With no color it shows the transparency checker, and a translucent value layers over it. Flat by default; shadow adds the kit's hard drop shadow.

Properties
PropertyTypeDefaultNotes
colorstring (CSS color)The fill. Unset shows the checker
width, heightnumber24, 18The border box, in whole system px
shadowbooleanfalseCast the kit's hard drop shadow
labelstring''Accessible name; defaults to the color, or "transparent"
disabledbooleanfalseStops interaction; nothing dims — a swatch has no label to dim
Anatomy
KindNameNotes
Partsbutton, fillNo slots
EventsNone custom — native click. Not form-associated: a palette cell picks, it does not submit
Rolenative <button>Enter and Space activate
Token--vf-swatch-checkerThe transparency-checker tile; --vf-shadow-offset for the depth under shadow

vf-checkbox

A 13×13 white box with the corner-to-corner ✕. Pressing thickens the border to 2px.

Properties
NameTypeDefaultNotes
checkedbooleanfalseReflected
disabled, nameStandard; form-associated. disabled dims the label only, drops the tab stop, and follows an ancestor <fieldset disabled>
valuestring'on'Submitted when checked, as on a native checkbox
vf-changeEvent{ checked: boolean }, on user interaction
Slot: default (the label). Parts: box, label. Role checkbox; Space toggles.

vf-radio & vf-radio-group

A 13×13 circle: a 1-bit outline over a white disc. The group is the form control: it owns value, the roving tabindex and the submitted name. A bare vf-radio outside a group manages itself.

vf-radio-group
NameTypeDefaultNotes
valuestring''The selected child's value; setting it checks that child
name, disabledStandard. Form-associated: the group submits, not the radios
labelstring''Accessible name (or use vf-label for / aria-labelledby)
vf-changeEvent{ value }
Slot: default — the radios, or arbitrary markup containing them. Role radiogroup; arrows move and select, on a roving tabindex. No parts.
vf-radio
NameTypeDefaultNotes
valuestring''What the group reports when this one is selected
checkedbooleanfalseReflected. Inside a group, the group is the source of truth
disabledbooleanfalseDims the label; the ring and dot stay black
vf-changeEvent{ value }, on user interaction only
Slot: default (the label). Parts: circle, label. Not form-associated by design — the group is.

vf-text-field

A bordered text input, 22px tall, typed in the chrome face. Selected text inverts to the 1-bit selection all three editable fields share.

Properties
NameTypeDefaultNotes
value, placeholder, name, disabled, readonly, typeStandard; form-associated — a plain <form> submits it, form.elements holds it, form.reset() restores it. type defaults to 'text' and passes through to the inner input
labelstring''Accessible name, for a field with no visible caption. Left empty, a host-level aria-label/aria-labelledby or an associated <label for> is bridged in instead
requiredbooleanfalseReal constraint validation: reportValidity() blocks with the native message, :invalid matches on the host, and Enter cannot submit past it
descriptionstring''Hint text for assistive tech (aria-describedby can't cross the shadow boundary on its own); a failing constraint's message joins it
vf-inputEvent{ value } on every keystroke
vf-changeEvent{ value } on commit
--vf-field-widthToken180pxDefault width. Set a width on the host or this token, not on ::part(input)
Part: input. Also --vf-control-height (22px). The focus rule paints 2 system px below the host's own box, so a tight overflow: hidden ancestor clips it.

vf-text-area

The multi-line field: everything vf-text-field takes, plus rows. It reserves a permanent scroll rail — an empty white channel until the text overflows.

Properties
NameTypeDefaultNotes
rowsnumber4Visible lines, as on <textarea>
Everything else matches vf-text-field: value, placeholder, name, disabled, readonly, label, required, description, vf-input, vf-change, --vf-field-width. Part: textarea.

vf-number-field

A numeric field with the "little arrows" stepper. The well is 22 system px, the same as the other fields; the 25-px stepper beside it sets the host's height.

Properties
NameTypeDefaultNotes
min, maxnumberClamp the value; also fill aria-valuemin/max
stepnumber1Step size and the value's decimal precision
value, placeholder, name, disabled, readonly, labelStandard. readonly blocks stepping as well as editing
vf-input / vf-changeEvent{ value, valueAsNumber } — on every keystroke / on commit or a step
--vf-number-field-widthToken4emWidth of the well
Parts: input, stepper. Role spinbutton on the input; ↑/↓ step, Home/End jump to min/max.

vf-select & vf-option

The popup menu control. Two pointer styles: press-drag-release (press the pill, drag onto an item, release to pick) and click-to-open (a quick tap leaves the list open for a second click). Selecting blinks the row, then closes.

The panel opens with the selected row over the closed pill, and the control sizes to its widest option. Set min-width on the host for a floor.

size="small" sets the pill and its list in the body face, on a 12px pill with 10px rows.

no-shadow draws the pill without its 1px shadow. The open list keeps its shadow.

A list taller than the screen is clipped, not scrolled. An edge row with items beyond it shows a solid arrow; rest the pointer on it and the list rolls a row at a time. A clipped panel still opens with its selected row over the pill. Set --vf-popup-inset-top once on :root to keep clamped panels clear of a menu bar (24px for a vf-menu-bar).

vf-select
NameTypeDefaultNotes
valuestring''The selected option's value
name, disabledStandard; form-associated. Disabled dims the label only — the ▼ stays black
labelstring''Accessible name
vf-changeEvent{ value }, after the blink
Slot: default (vf-option elements). Parts: control, label, arrow, panel, scroll-arrow. Tokens: --vf-popup-height (18px), --vf-select-gutter (16px — the ✓ column, shared with the menus), --vf-popup-inset-top / --vf-popup-inset-bottom (4px — the screen-edge reserve a clipped panel keeps clear; declare once on :root). Role combobox over a listbox panel; Space, Enter or ↓ open, arrows move, Enter or Space select, Esc cancels, Home/End jump. The panel is position: fixed, so it escapes clipping containers.
vf-option
NameTypeDefaultNotes
valuestring''What the select reports
disabledbooleanfalseGrayed and unpickable
selectedbooleanfalseReflected; managed by the parent
activebooleanfalseThe transient press-drag highlight, managed by the parent
Slot: default (the label). Part: check. Role option.

vf-progress-bar

Determinate: a solid black fill from the left with a 1px leading edge. Indeterminate: full-width diagonal barber stripes, stepped rather than smoothed. Give the host a width.

Properties
NameTypeDefaultNotes
valuenumber00–max
maxnumber100
indeterminatebooleanfalseBarber stripes; aria-valuenow is omitted
labelstring''Accessible name — the only way to name an indeterminate bar
Parts: track, fill. Tokens: --vf-progress-fill, --vf-progress-track, --vf-progress-stripes. Role progressbar. No events — it is an output.

vf-slider

A horizontal slider: a 4px capsule rail, solid black up to the shield-shaped handle and hollow after it. A 1-bit adaptation of the later Mac OS control, not a System 7 original. Give the host a width.

Properties
NameTypeDefaultNotes
valuenumber0
min, max, stepnumber0, 100, 1Standard. Arrows step by step, Page keys by max(step, range/10)
name, disabledStandard; form-associated. Disabled dims the whole control — the fill is the value, and there is no label to dim instead
labelstring''Accessible name
vf-input / vf-changeEvent{ value: number } — every drag move or key / on commit
Parts: track, rail, thumb. Role slider. Give the host a width.

vf-menu-bar, vf-menu & vf-menu-item

Pull-down menus with press-drag-release: press a title, slide onto a command, release to run it. Sliding onto another title switches menus mid-press; releasing over a disabled row, a separator or off the menu chooses nothing. A quick tap leaves the menu open for a second click. Selecting blinks the row (skipped under prefers-reduced-motion), then fires vf-menu-select.

A vf-menu also works standalone:

vf-menu-bar
KindNameNotes
PropertiesroundedBoolean; paints the 5px screen-corner stair-step on the bar's top corners. Otherwise the bar is the coordinator: one menu open at a time, hover switches while open, ArrowLeft/Right move between menus
PropertiesshortcutsBoolean; makes every item's shortcut a live key equivalent, bar-wide. Off by default — key equivalents are page-global, so only the menu bar should declare it
Slot(default)vf-menu elements
PartbarStyle this to align a full-bleed nav with a centered content column
Token--vf-menubar-height20px — 19 white system px over the 1px black rule
vf-menu
NameTypeDefaultNotes
labelstring''The title in the bar, and the accessible name when the label slot replaces the text
openbooleanfalseReflected; managed by the bar, or by the menu itself when standalone
shortcutsbooleanfalseThe key-equivalent grant for a standalone menu; in a bar, declare it once on the bar
Slots: default (vf-menu-item / vf-separator), label. Parts: label, panel. Roles menubar / menu / menuitem (menuitemcheckbox when checkable); arrows move, Enter or Space activate, Esc closes.
vf-menu-item
NameTypeDefaultNotes
valuestringtext contentCarried in the event detail
shortcutstring''Key equivalent (⌘N), left-aligned in a shared right-anchored column so every ⌘ lines up, announced via aria-keyshortcuts. Under a shortcuts grant on the menu or bar it is live: the matching keydown activates the item from anywhere, menu open or not, and a closed menu flashes its title. A disabled item claims nothing; a claimed stroke is preventDefault()ed; a bare letter never matches
checkedbooleanfalseShows the ✓ in the left gutter
checkablebooleanfalseA toggle that starts unchecked; announces as menuitemcheckbox
disabledbooleanfalseDims the whole row, ✓ included, as System 7 did
activebooleanfalseThe transient press-drag highlight, managed by the menu
activate()MethodBlink, then select — what the press gesture calls for the row a drag was released over
vf-menu-selectEvent{ value, item }
Slot: default (the label). Parts: item, check, label, shortcut. Token: --vf-menu-row-height (16px), --vf-menu-shortcut-column (23px), --vf-select-gutter.

vf-list & vf-list-item

The list box: inverted selection, a permanent scroll rail, and first-letter type-ahead. Each row takes a leading graphic in its icon slot, usually a 16×16 vf-img.

vf-list
NameTypeDefaultNotes
multiplebooleanfalseShift/⌘ extend, Space toggles
valuestring''The selected row's value (the first, when multiple)
valuesstring[][]Property only — no attribute
disabledbooleanfalsePushes aria-disabled down onto every row, and re-enabling leaves individually disabled rows disabled
labelstring''Accessible name for the listbox
vf-changeEvent{ value, values }
--vf-list-max-heightToken200pxPart: list. Give the host a width
Roles listbox / option. Arrows move selection, Space toggles when multiple, printable keys drive first-letter type-ahead.
vf-list-item
NameTypeDefaultNotes
valuestring''Reported by the list
selectedbooleanfalseReflected; the row inverts full width
disabledbooleanfalseSkipped by selection and by type-ahead
Slots: default (the row text), icon (a leading graphic; contributes no text to type-ahead). Part: text (it ellipsizes). Keep row height − icon height even, so the centering offset stays a whole pixel.

vf-scroll-area

A container with System 7 scrollbars: 16px cells, a dot-dither trough, the white elevator, and hollow arrow glyphs that fill solid when pressed. A reserved axis keeps its rail as a permanent empty white channel and fills it in only when the content overflows. In a non-frontmost vf-window, both rails blank. For rails on a window's own frame rather than inset in its body, use vf-window[scrollbars].

Properties
NameTypeDefaultNotes
axis'vertical' | 'horizontal' | 'both''vertical'Which rails to reserve. The unreserved axis stays on-demand
cornerbooleanfalseReserves the 15×15 corner cell beside a single rail, for a grow box to land in. both always has one
labelstring''Accessible name for the focusable viewport. A non-empty value also promotes it to role="region"; the role is omitted while empty
measure()methodRe-measures overflow and re-syncs the rails. Content growth, slot changes and a placed child moved through top/left are tracked by themselves; call this for a scroll range that changes with none of those, such as a transform
placementAt(x, y)methodA viewport point (CSS px) as { left, top } on the scrolled plane, whole system px on the placement lattice, scroll offset included — the pair to write onto a child dropped here
Slot: default. Part: viewport — focusable, driven by the arrow and Page keys. Set width and height on the host. The scrolled plane is never narrower than the viewport and grows to content that cannot wrap, so a row gaining a cell moves the thumb; a sticky child holds across the scroll. The scroller does not rubber-band; on a page that scrolls, restore wheel chaining with ::part(viewport) { overscroll-behavior: auto }. The rails are kit-drawn shadow DOM, the same in every engine.

vf-fieldset

The group box, with the legend punching through the top border. The legend's backdrop comes from --vf-surface (white by default; vf-window/vf-dialog set it) — on a tinted background, set it on the container.

Properties
NameTypeDefaultNotes
legendstring''The caption; the legend slot overrides it
Slots: default, legend. Parts: fieldset, legend. Role group. Token: --vf-surface.

vf-grid

A lattice of equal cells with 1px rules between them: a tool palette, a swatch table, a pattern chooser. Solid and dashed pens share one layout, and the rules run behind the cells.

A cell is centered in its well on both axes; place-self: stretch fills the well instead, and is the default under collapse. The grid takes no role, keyboard behavior or selection — put role="group" or role="radiogroup" on the host as the content warrants.

Properties
NameTypeDefaultNotes
columnsnumber1Cells across
rowsnumberCells down. Unset, the slotted cells decide; set, that many are reserved, so an unfilled cell still gets its rules
cell-width, cell-heightnumber16Whole system px. Properties are cellWidth / cellHeight
rules'solid' | 'dashed' | 'none''solid'The pen. none closes the gap and the cells butt together
framelessbooleanfalseDrop the perimeter, for a grid whose container already draws that line
collapsebooleanfalseborder-collapse: collapse in system px: pulls each cell back 1px on all four sides so its border and the rule become one line
Slot: default (the cells, in order — they keep their own semantics and styling). Parts: grid, rules. Tokens: --vf-black, --vf-surface. No events.

vf-stack

A flexbox whose gap, pad, width and height are declared in whole system pixels — one of the two stylesheet-free layouts; the other is coordinate placement, below.

A page stylesheet cannot state system pixels: each element sets --vf-scale on itself, never on the document, so var(--vf-scale, 1) outside a vf-* ancestor silently resolves to 1.

Content governs the box: a column is as wide as its widest child, a row as tall as its tallest, and children keep the size they were drawn at. The stack paints nothing, takes no role, and returns face, line box, color and selectability to inherit.

Properties
NameTypeDefaultNotes
direction'column' | 'row''column'A stack is vertical unless it says otherwise
gapnumber0Whole system px between children
padstring | numberWhole system px, one to four values in CSS shorthand order (pad="12", pad="10 12", pad="14 12 10"). The way to inset a vf-window or vf-scroll-area's content, which carry no inset of their own
place'start' | 'center' | 'end'per directionWhere the children sit across the stack. Unset resolves to start down a column and center across a row. There is no justify. Centering that would land on a half system px is stepped back onto a whole one, the half toward the start
width, heightnumberWhole system px; optional — the content otherwise decides. In a row, a declared width is what creates the slack a child's fill-width divides
fill-width, fill-heightbooleanfalseBare attributes on a child: be as wide (tall) as this stack allows. fill-width always works in a column and needs a declared width in a row; fill-height is the reverse. A fill with nothing to take is inert; two children filling along the main axis come out equal. A stack also reads both about itself, for parents that aren't stacks
Slot: default. Children are flex: 0 0 auto; your own align-self: stretch overrides it. No parts, tokens, events or role.

The other way: top and left

Every component takes top and left in whole system px. Set either and the element is absolutely positioned within its parent, the missing coordinate defaulting to 0; set neither and it renders in flow. (0,0) is the nearest positioned ancestor — every kit container is one; a container of your own needs position: relative, or use a vf-container, below.

That includes the rows a container usually owns — vf-option, vf-menu-item, vf-list-item and a bar's vf-menu. A placed child leaves the parent's flow: a select or menu panel no longer counts it toward its width, the popup's scroll clamp skips it, and the rows after it close the gap.

vf-dialog takes the pair in viewport coordinates — the top layer's containing block is the viewport — and unset means centered. A drag writes through the same two properties, so read a moved element's position off el.left, not style.left, which holds a live calc().

fixed holds the placement against the visible region of the nearest scrolling ancestor instead of its scrolled plane: the element keeps its top/left while the content scrolls under it, and the flag alone places it at (0,0). A fixed child comes before the flow content in its parent and paints over placed siblings. Where nothing scrolls it renders as placed.

origin names which point of the element's own box the pair places: nine keywords, vertical then horizontal — top left (the default), top center, top right, center left, center, center right, bottom left, bottom center, bottom right. The point is measured in whole system px and kept current as the box changes, an odd size's leftover half going toward the start; it is never a transform. A drag keeps the point, and placementAt(x, y, child) returns the pair that lands a dropped child's corner there. The Page Setup dialog under vf-dialog centers its title on left="145" and holds its button row's bottom-right corner at (274, 168) this way.

vf-container

A box that is nothing but its declared size: width and height in whole system px around anything you slot into it. It paints nothing but what pattern and rule name, and has no layout opinion — in-flow children get normal flow, placed children get a coordinate system.

Properties
NameTypeDefaultNotes
width, heightnumberWhole system px. Content that outgrows the box overflows it rather than growing it; an omitted axis shrink-wraps
patternstringA library pattern by name (bricks, gray-50, … — the 38 standard MacPaint patterns, shown above) or sixteen hex digits stating a custom 8×8 pattern row by row, bit 7 the leftmost pixel, 1 = ink. Painted as the box's background, black on --vf-white, anchored at the box's top-left, 1-bit at every density. A declared axis sizes the raster; an undeclared one is measured. Unset paints nothing; an unrecognized value paints nothing and warns once. Flat under forced colors
rulestringEdge names separated by spaces — bottom, top bottom, up to all four in any order. Draws the 1px rule on those edges as the box's own border, inside the declared size, in --vf-black: a 24px rule="bottom" strip is 23 rows of box over one of ink. Content, fill-width children and placed children begin inside the rule. Unset draws none; a value naming anything but an edge draws none and warns once
fill-width, fill-heightbooleanfalseBare attributes, read two ways as in vf-stack: on a child, fill to this box (fill-height needs a declared height to resolve against; a fill with nothing to take is inert); on the container itself, fill whatever parent can give it a size
Slot: default. Takes top/left, so a container is itself placeable. It corrects its own origin, carrying everything placed inside it — kit or not — with the coordinate system. Typographically transparent like vf-stack. No parts, tokens, events or role.

vf-label

The static caption: "Name:" beside a field, "Mode" over a radio group. Chrome type on a 16px line box, stated in whole system pixels.

for works like a native <label for>: clicking focuses the target, and the caption text becomes its accessible name. It never overwrites a name you set.

Properties
NameTypeDefaultNotes
forstringId of the labelled control, resolved in the label's own tree scope. A disabled target is not focused; a target that has not upgraded yet is waited for
face'display' | 'body''display'Chrome type by default
dimbooleanfalseGreys the text to --vf-disabled
widthnumberWhole system px — a caption column's shared width, so a run of label-and-field rows lands every field on one x. A caption wider than its width overflows rather than reflowing the row
Slot: default. Part: label. Line box: the face's native line — --vf-line-height-display (16px, Chicago 12's) or --vf-line-height (12px, Geneva 9's) per face, with --vf-label-line-height overriding both for captions alone. display: inline-block, so a page can give a caption column a shared width.

vf-paragraph

A run of copy on the kit's body face and its native 12px line; the display face wraps at 16px. No margin — paragraph spacing is the page's.

Properties
NameTypeDefaultNotes
face'display' | 'body''body'
dimbooleanfalse
Slot: default. Part: paragraph. Line box: the face's native line — --vf-line-height (12px) or --vf-line-height-display (16px) per face, with --vf-paragraph-line-height overriding both for paragraphs alone. Selectable, unlike the kit's chrome text.

vf-img

Pixel art on the kit's grid: a slotted <img> sized to one system pixel per image pixel, magnified nearest-neighbor on whole device pixels. The graphic stays a native <img> in your light DOM — alt, srcset and loading behavior are untouched.

Properties
NameTypeDefaultNotes
width, heightnumberthe image's natural sizeWhole system px. Stated up front, they reserve the box before the file loads; until a size is known the box is 0×0
Slot: default (a single <img> — use alt="" when it is decorative, as in a list row or a menu title that already carries a name). Part: frame.

vf-icon

The Finder icon: a picture and a caption that select, move and rename together.

The art arrives by slot: large is the 32×32 ICN#, small the 16×16 ics#. size picks which one paints and the cell it paints in. The cell is reserved whether or not the art fills it, so a row of icons keeps one baseline. A field of icons sits in a vf-icon-field, which is what makes a selectable icon an option.

color declares the art a color icon: selection darkens it — every color blended halfway toward black — instead of inverting it into a negative. The plate inverts either way.

open — the icon's window is on screen — redraws the art as a ghost: outline held, interior re-filled with the kit's dither. It is derived from the slotted art — no second file, and cross-origin art works.

A name is never abbreviated or folded. Wider than its cell, it overflows, centered: width is the cell pitch, not a bound on the name. A declared width must be even.

A drag draws the outline on the desktop, over windows, palettes and the menu bar alike, and is reported rather than interpreted: vf-drag-start, vf-drag on every step, and a cancelable vf-drop whose default action is the move. The page decides what a drop means — the kit ships no filing semantics — and cancels the default action when it files the icon itself. target paints the destination highlight on the folder under the pointer; placementAt() on the destination converts the outline's origin into that container's own coordinates.

Properties
NameTypeDefaultNotes
labelstring''The name. Empty draws no plate
sizelarge | smalllargePicks the slot that paints and the cell it paints in: 32×32 or 16×16. A vf-icon-field with a size writes it onto every icon inside
selectablebooleanfalseClicking selects and the icon becomes focusable; movable and editable presuppose it. Set selected yourself to drive selection some other way
selectedbooleanfalseReflected; the art inverts and the plate takes the --vf-highlight pair
colorbooleanfalseDeclares the art a color icon: selection darkens it — every color blended halfway toward black, whites going gray — instead of inverting it into a negative. The plate inverts either way
openbooleanfalseThe open ghost: outline held, interior re-filled with the kit's dither. Derived from the art you already slotted — no second file. Selection inverts the ghost the same way
targetbooleanfalseReflected; the destination highlight under a drag: the art's selected treatment alone, with no event, no outside-press listener, no aria-selected and the plate untouched
movablebooleanfalseDrag to move, plus arrow keys (Shift for eight). The drag fires the four events below; Escape mid-drag cancels it
editablebooleanfalseClick the plate of an already-selected icon to rename; the field opens once the double-click window has passed, so a double-click, or two taps, opens the icon instead
widthnumberthe wider of cell and nameSystem px, and even. The cell pitch; a longer name overflows it
maxlengthnumber31The HFS filename limit, applied to the rename field. A label set from your own data is never truncated
setSelected(next)methodSelect or deselect as a press would, vf-select included. A selected write from code is silent
touches(box)methodWhether a viewport box reaches the art cell or the name plate — the selection rectangle's test; the empty cell beside them counts for nothing
moveTo(left, top)methodPlace as a drop would: clamped whole in the container (held at the origin only in a scrolling plane, whose rails reach the rest), snapped to the lattice, one vf-placement-change. A left/top write is the authored pair, unclamped
dragTo(left, top)methodMove the way a drag moves: the dotted outline travels from where the icon sits to where it will land, and the icon lands when the outline arrives, written as moveTo writes. A promise, resolved once it has landed — true when the outline travelled, false when the icon landed at once (already there, or reduced motion). Fires no drag event and needs no movable. A press anywhere, or Escape, lands it at once
vf-selectEvent{ selected }
vf-changeEvent{ label, previous } — the name was committed
vf-openEvent{} — double-clicked anywhere on the icon, name included; tapped twice with a finger or pen (the second tap within 500ms and 24 CSS px of the first); or ⌘O / ⌘↓ (Ctrl off the Mac). Return renames instead
vf-name-too-longEvent{ attempted, accepted, limit } — a rename went past maxlength
vf-name-rejectedEvent{ attempted, kept, reason } — a rename committed empty; the old name is kept and no vf-change fires
vf-drag-startEvent{ left, top, icons } — the first lattice step past the drag slop, 4 CSS px from the press for the mouse and 10 for a finger or pen; the origin in the icon's container, and every icon travelling (this one first: a drag on a selected icon carries the field's other selected icons). A press released inside the slop is a click or a tap and fires nothing
vf-dragEvent{ clientX, clientY, left, top, x, y, icons } on every step that changed the proposal: the pointer in CSS px, the proposed origin in the container (whole system px on the lattice, unclamped), the outline's corner in viewport CSS px for a drop into another container, and the icons travelling
vf-dropEventThe release, cancelable, with the same detail. The default action writes the proposal through left/top — every icon in icons by the same delta, clamped whole in the container as a group, or held at the origin only in a scrolling plane; preventDefault() writes nothing, and re-parenting inside the handler is safe
vf-drag-cancelEvent{} — Escape or a pointercancel ended the drag; nothing was written
--vf-icon-gapToken0pxCell to plate — none by default, the plate directly under the cell
--vf-icon-label-heightToken12pxThe plate's line box
Slots: large (32×32 art, normally a vf-img), small (16×16). Parts: frame, icon, label, plate, input. Both files fetch even though one paints — use data URIs, or slot only the size in use. Inside a vf-icon-field a selectable icon is an option with aria-selected; unowned, it is role="img" named from its label.

vf-icon-field

The container a field of icons sits in: a desktop's icons, a folder window's contents. A multiselectable listbox named from label, so a selectable icon inside is an option with aria-selected. It paints nothing and, until placed or filled, takes no size and no position of its own: a field of placed icons is a zero-height block, and the icons anchor to the desktop's raster or the window's plane as if it were not there.

Properties
NameTypeDefaultNotes
labelstring''The listbox's accessible name, through internals — a role or aria-* of your own on the tag wins
sizelarge | smallWritten onto every icon in the field, on arrival and whenever it changes. Unset, each icon keeps its own
top, left, width, heightnumberSystem px. Stated, the field is a box and the anchor its icons place against
fill-width, fill-heightbare attributeBe as wide (tall) as the parent allows: the desktop's screen, a window body. The field stays static, so its icons keep their anchor; it gains a surface to press
dragIcons(moves)method{ icon, left, top }[] — the Finder's Clean Up: each icon's outline travels to its pair and the icon lands as it arrives, one icon at a time in the array's order, the kit's beat between landings. A promise, resolved when the last has landed. A second call finishes the first; a press anywhere, or Escape, lands every remaining icon at once; under reduced motion everything lands at once with no outline
Slot: default (the icons). Always aria-multiselectable. A vf-desktop renders no field of its own; the page writes one beside the windows, and a folder window holds one in its body. A drag on a filled or placed field's own background is the rubber band: an icon is selected exactly when it was selected at the press XOR the rectangle touches its art or its name, each change firing vf-select — a plain press clears first, so the rectangle selects what it touches; Shift or ⌘ keeps the selection and the rectangle toggles against it. Escape cancels. The rectangle draws at the field's own level — under the windows on a desktop, inside the window in a body — and reaches no further than the field inside its clips.

Focus

The components are good citizens of the modern web: focusable, keyboard-operable, and named for assistive tech, like the native elements they stand in for. Keyboard access postdates System 7, so the focus mark is the kit's own, added in the 1-bit idiom — a 1px dashed rule under the focused control. It is keyboard-only: a mouse click never draws it.