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.
| Kind | Name | Notes |
|---|---|---|
| Property | width, height | The screen raster, in system px (defaults 512×342). The host box is the screen plus 2×bezel per axis |
| Property | bezel | The 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 |
| Property | pattern | The 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 |
| Property | activeWindow | Read-only: the active document-tier window, or null while the tier is deactivated or empty. Utility windows are never the holder |
| Method | fitWithin(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 |
| Method | clearActive() | Deactivates the document tier. A press or focus in a document window, or a newly slotted one, reactivates; utility windows keep their active |
| Method | placementAt(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 |
| Event | vf-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 |
| Part | desktop | The dithered surface |
| Token | --vf-desktop | Base color under the tile (#808080) |
| Token | --vf-desktop-pattern | The 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.
| Archetype | Recipe |
|---|---|
| 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> |
| Property | Type | Default | Notes |
|---|---|---|---|
width | number | — | Whole system px. Declare it, with height; undeclared, the window falls back to plain block layout and warns once in the console |
height | number | — | Whole 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 |
heading | string | '' | Title text, centered on the stripes. Also qualifies the widgets' accessible names (Close Read Me) |
active | boolean | true | Frontmost. 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 |
closable | boolean | true | The left close box |
zoomable | boolean | false | The right zoom box |
movable | boolean | false | Drag the title bar to move; the drag writes top/left in system px |
resizable | boolean | false | The 15×15 grow box, bottom right; writes width/height within the size rect below and fires vf-resize |
min-width, min-height | number | 80, 54 | Whole system px. The narrowest and shortest the grow box will make the window. Properties are minWidth / minHeight |
max-width, max-height | number | — | Whole 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-height | number | — | Whole 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) | method | — | A 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() | method | — | Re-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 |
| Kind | Name | Notes |
|---|---|---|
| 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 |
| Slot | header | The 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 |
| Slot | status | The 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 |
| Role | group | Named by its heading |
| Event | vf-close | The close box was clicked. The window does not remove itself — the consumer decides |
| Event | vf-zoom | The zoom box was clicked |
| Event | vf-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 |
| Parts | frame, title-bar, title, close-box, zoom-box, header, body, status-bar, grow-box | Plus 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-offset | 18px / 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.
| Property | Type | Default | Notes |
|---|---|---|---|
open | boolean | false | Reflected, and kept in sync with the native dialog both directions |
width | number | — | Whole 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 |
height | number | — | Whole system px; content taller than the box clips at the frame. Unset, the body sizes to its content and warns once in the console |
heading | string | '' | Title-bar text, and the accessible name. On frame="plain", which has no bar, it only names the dialog |
label | string | '' | Accessible name for a dialog with no heading; wins over one when both are set. Falls back to 'Dialog' |
closable | boolean | false | Opt into the close box. Ignored under frame="plain", which has no bar |
light-dismiss | boolean | false | A 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 |
| Kind | Name | Notes |
|---|---|---|
| Method | show() | Opens it (native showModal()), pinned onto the device-pixel grid |
| Method | close() | Closes it; fires vf-close with reason: 'close' |
| Event | vf-close | { reason: 'escape' | 'close' | 'outside' } — every close path routes through the native close event. 'outside' only under light-dismiss |
| Keyboard | Initial focus | On 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") |
| Keyboard | Return / Enter | Activates 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 |
| Parts | frame, title-bar, title, close-box, body, content | Title 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.
| Name | Type | Default | Notes |
|---|---|---|---|
vertical | boolean | false | 1px wide, auto height — give it a height context |
--vf-separator-color | Token | --vf-black | Menus set it to the dim gray |
--vf-separator-style | Token | solid | Menus 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.
| Property | Type | Default | Notes |
|---|---|---|---|
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 |
disabled | boolean | false | Dims 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, value | string | '' | 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') |
| Kind | Name | Notes |
|---|---|---|
| Slot | (default) | The label |
| Part | button | The inner <button> — put a min-width here |
| Events | — | None custom; native click suffices |
| Role | native <button> | Enter and Space activate |
| Tokens | --vf-button-height (20px), --vf-button-ring-margin, --vf-button-flex | The 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.
| Name | Type | Default | Notes |
|---|---|---|---|
vertical | boolean | false | Stack in a column instead of a row |
natural | boolean | false | Let each button keep its own content width |
--vf-button-group-gap | Token | 12px | Gap 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.
| Property | Type | Default | Notes |
|---|---|---|---|
color | string (CSS color) | — | The fill. Unset shows the checker |
width, height | number | 24, 18 | The border box, in whole system px |
shadow | boolean | false | Cast the kit's hard drop shadow |
label | string | '' | Accessible name; defaults to the color, or "transparent" |
disabled | boolean | false | Stops interaction; nothing dims — a swatch has no label to dim |
| Kind | Name | Notes |
|---|---|---|
| Parts | button, fill | No slots |
| Events | — | None custom — native click. Not form-associated: a palette cell picks, it does not submit |
| Role | native <button> | Enter and Space activate |
| Token | --vf-swatch-checker | The 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.
| Name | Type | Default | Notes |
|---|---|---|---|
checked | boolean | false | Reflected |
disabled, name | — | — | Standard; form-associated. disabled dims the label only, drops the tab stop, and follows an ancestor <fieldset disabled> |
value | string | 'on' | Submitted when checked, as on a native checkbox |
vf-change | Event | — | { 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.
| Name | Type | Default | Notes |
|---|---|---|---|
value | string | '' | The selected child's value; setting it checks that child |
name, disabled | — | — | Standard. Form-associated: the group submits, not the radios |
label | string | '' | Accessible name (or use vf-label for / aria-labelledby) |
vf-change | Event | — | { value } |
Slot: default — the radios, or arbitrary markup containing them. Role radiogroup; arrows move and select, on a roving tabindex. No parts. | |||
| Name | Type | Default | Notes |
|---|---|---|---|
value | string | '' | What the group reports when this one is selected |
checked | boolean | false | Reflected. Inside a group, the group is the source of truth |
disabled | boolean | false | Dims the label; the ring and dot stay black |
vf-change | Event | — | { 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.
| Name | Type | Default | Notes |
|---|---|---|---|
value, placeholder, name, disabled, readonly, type | — | — | Standard; 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 |
label | string | '' | 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 |
required | boolean | false | Real constraint validation: reportValidity() blocks with the native message, :invalid matches on the host, and Enter cannot submit past it |
description | string | '' | Hint text for assistive tech (aria-describedby can't cross the shadow boundary on its own); a failing constraint's message joins it |
vf-input | Event | — | { value } on every keystroke |
vf-change | Event | — | { value } on commit |
--vf-field-width | Token | 180px | Default 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.
| Name | Type | Default | Notes |
|---|---|---|---|
rows | number | 4 | Visible 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.
| Name | Type | Default | Notes |
|---|---|---|---|
min, max | number | — | Clamp the value; also fill aria-valuemin/max |
step | number | 1 | Step size and the value's decimal precision |
value, placeholder, name, disabled, readonly, label | — | — | Standard. readonly blocks stepping as well as editing |
vf-input / vf-change | Event | — | { value, valueAsNumber } — on every keystroke / on commit or a step |
--vf-number-field-width | Token | 4em | Width 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).
| Name | Type | Default | Notes |
|---|---|---|---|
value | string | '' | The selected option's value |
name, disabled | — | — | Standard; form-associated. Disabled dims the label only — the ▼ stays black |
label | string | '' | Accessible name |
vf-change | Event | — | { 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. | |||
| Name | Type | Default | Notes |
|---|---|---|---|
value | string | '' | What the select reports |
disabled | boolean | false | Grayed and unpickable |
selected | boolean | false | Reflected; managed by the parent |
active | boolean | false | The 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.
| Name | Type | Default | Notes |
|---|---|---|---|
value | number | 0 | 0–max |
max | number | 100 | |
indeterminate | boolean | false | Barber stripes; aria-valuenow is omitted |
label | string | '' | 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.
| Name | Type | Default | Notes |
|---|---|---|---|
value | number | 0 | |
min, max, step | number | 0, 100, 1 | Standard. Arrows step by step, Page keys by max(step, range/10) |
name, disabled | — | — | Standard; form-associated. Disabled dims the whole control — the fill is the value, and there is no label to dim instead |
label | string | '' | Accessible name |
vf-input / vf-change | Event | — | { value: number } — every drag move or key / on commit |
Parts: track, rail, thumb. Role slider. Give the host a width. | |||
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.
| Name | Type | Default | Notes |
|---|---|---|---|
multiple | boolean | false | Shift/⌘ extend, Space toggles |
value | string | '' | The selected row's value (the first, when multiple) |
values | string[] | [] | Property only — no attribute |
disabled | boolean | false | Pushes aria-disabled down onto every row, and re-enabling leaves individually disabled rows disabled |
label | string | '' | Accessible name for the listbox |
vf-change | Event | — | { value, values } |
--vf-list-max-height | Token | 200px | Part: list. Give the host a width |
Roles listbox / option. Arrows move selection, Space toggles when multiple, printable keys drive first-letter type-ahead. | |||
| Name | Type | Default | Notes |
|---|---|---|---|
value | string | '' | Reported by the list |
selected | boolean | false | Reflected; the row inverts full width |
disabled | boolean | false | Skipped 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].
| Name | Type | Default | Notes |
|---|---|---|---|
axis | 'vertical' | 'horizontal' | 'both' | 'vertical' | Which rails to reserve. The unreserved axis stays on-demand |
corner | boolean | false | Reserves the 15×15 corner cell beside a single rail, for a grow box to land in. both always has one |
label | string | '' | Accessible name for the focusable viewport. A non-empty value also promotes it to role="region"; the role is omitted while empty |
measure() | method | — | Re-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) | method | — | A 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.
| Name | Type | Default | Notes |
|---|---|---|---|
legend | string | '' | 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.
| Name | Type | Default | Notes |
|---|---|---|---|
columns | number | 1 | Cells across |
rows | number | — | Cells down. Unset, the slotted cells decide; set, that many are reserved, so an unfilled cell still gets its rules |
cell-width, cell-height | number | 16 | Whole system px. Properties are cellWidth / cellHeight |
rules | 'solid' | 'dashed' | 'none' | 'solid' | The pen. none closes the gap and the cells butt together |
frameless | boolean | false | Drop the perimeter, for a grid whose container already draws that line |
collapse | boolean | false | border-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.
| Name | Type | Default | Notes |
|---|---|---|---|
direction | 'column' | 'row' | 'column' | A stack is vertical unless it says otherwise |
gap | number | 0 | Whole system px between children |
pad | string | number | — | Whole 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 direction | Where 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, height | number | — | Whole 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-height | boolean | false | Bare 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.
| Name | Type | Default | Notes |
|---|---|---|---|
width, height | number | — | Whole system px. Content that outgrows the box overflows it rather than growing it; an omitted axis shrink-wraps |
pattern | string | — | A 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 |
rule | string | — | Edge 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-height | boolean | false | Bare 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.
| Name | Type | Default | Notes |
|---|---|---|---|
for | string | — | Id 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 |
dim | boolean | false | Greys the text to --vf-disabled |
width | number | — | Whole 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.
| Name | Type | Default | Notes |
|---|---|---|---|
face | 'display' | 'body' | 'body' | |
dim | boolean | false | |
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.
| Name | Type | Default | Notes |
|---|---|---|---|
width, height | number | the image's natural size | Whole 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.
| Name | Type | Default | Notes |
|---|---|---|---|
label | string | '' | The name. Empty draws no plate |
size | large | small | large | Picks 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 |
selectable | boolean | false | Clicking selects and the icon becomes focusable; movable and editable presuppose it. Set selected yourself to drive selection some other way |
selected | boolean | false | Reflected; the art inverts and the plate takes the --vf-highlight pair |
color | boolean | false | Declares 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 |
open | boolean | false | The 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 |
target | boolean | false | Reflected; 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 |
movable | boolean | false | Drag to move, plus arrow keys (Shift for eight). The drag fires the four events below; Escape mid-drag cancels it |
editable | boolean | false | Click 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 |
width | number | the wider of cell and name | System px, and even. The cell pitch; a longer name overflows it |
maxlength | number | 31 | The HFS filename limit, applied to the rename field. A label set from your own data is never truncated |
setSelected(next) | method | — | Select or deselect as a press would, vf-select included. A selected write from code is silent |
touches(box) | method | — | Whether 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) | method | — | Place 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) | method | — | Move 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-select | Event | — | { selected } |
vf-change | Event | — | { label, previous } — the name was committed |
vf-open | Event | — | {} — 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-long | Event | — | { attempted, accepted, limit } — a rename went past maxlength |
vf-name-rejected | Event | — | { attempted, kept, reason } — a rename committed empty; the old name is kept and no vf-change fires |
vf-drag-start | Event | — | { 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-drag | Event | — | { 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-drop | Event | — | The 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-cancel | Event | — | {} — Escape or a pointercancel ended the drag; nothing was written |
--vf-icon-gap | Token | 0px | Cell to plate — none by default, the plate directly under the cell |
--vf-icon-label-height | Token | 12px | The 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.
| Name | Type | Default | Notes |
|---|---|---|---|
label | string | '' | The listbox's accessible name, through internals — a role or aria-* of your own on the tag wins |
size | large | small | — | Written onto every icon in the field, on arrival and whenever it changes. Unset, each icon keeps its own |
top, left, width, height | number | — | System px. Stated, the field is a box and the anchor its icons place against |
fill-width, fill-height | bare attribute | — | Be 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.