Vintage Frames

Install

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

Or import only the elements you use:

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

Sizes and positions are whole numbers of system px. A system px is one pixel of the original Mac screen, drawn as a whole number of device pixels.

vf-desktop

The gray desktop, which also manages the windows inside it. Pressing or focusing a vf-window brings it to the front and makes it active. DOM order follows the stacking order, updated after the press ends. Utility windows float above document windows and always stay active. A vf-menu-bar or vf-menu inside sits above both. A press on the bare desktop changes nothing. Call clearActive() to leave no document window active. Each change of active window fires vf-activate.

Size it with width and height, or with fitWithin(). CSS sizes don't apply. Position the desktop itself with your own CSS. Windows and icons inside use top and left.

Anatomy
KindNameNotes
Propertywidth, heightThe screen size in system px (default 512×342). The element adds the bezel on every side
PropertybezelA black border around the screen, in system px (default 0). Positions and clipping are measured from the screen. Its corners are rounded, so a menu bar inside doesn't need rounded
PropertypatternA pattern name (default gray-50) or 16 hex digits for a custom 8×8 pattern. The names are listed under vf-container. An unknown value keeps the default. --vf-desktop-pattern overrides it
PropertyactiveWindowRead-only. The active document window, or null. Never a utility window
MethodfitWithin(w, h)Sets and returns the largest size that fits in w × h CSS px, bezel included. Call it on resize and onScaleChange to fill the viewport
MethodbringToFront(win)Brings a window to the front, as a press would. A document window also becomes the active one; a utility window keeps its own active. DOM order follows once any press in progress ends
MethodclearActive()Deactivates every document window until the next press, focus, added window or bringToFront(). Utility windows stay active
MethodplacementAt(x, y)Converts a viewport point (CSS px) to { left, top } on the screen, in system px. Write the result to a child dropped there
Eventvf-activate{ window }: the new active window, or null. Fires only when it changes
Slot(default)Menu bar, windows, icon fields, anything. Drag outlines and window zoom animations inside draw on the screen, above the menu bar, clipped at its edge
PartdesktopThe screen
Token--vf-desktopThe color under the pattern (#808080)
Token--vf-desktop-patternThe pattern, as a background-image

vf-window

A window. The five classic Mac window types are each one line of markup.

Window types
TypeMarkup
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
widthnumber—Whole system px. Set it along with height. Unset, the window falls back to block layout and logs a warning
heightnumber—Whole system px. Taller content is clipped; use scrollbars to reach it. Unset, the body fits its content and logs a warning
headingstring''The title. Also used in the close and zoom boxes' accessible names (Close Read Me)
activebooleantrueDraws the window as the active one. An inactive window loses its stripes and its close and zoom boxes (which stay focusable), and its grow box and scrollbars are drawn empty. vf-desktop sets it
closablebooleantrueShows the close box
zoomablebooleanfalseShows the zoom box
movablebooleanfalseDrag the title bar to move it. The drag updates top and left
outline-dragbooleanfalseDrag a dotted outline instead of the window, and move the window on release. Escape cancels. Outside a vf-desktop the window moves live. Property: outlineDrag
resizablebooleanfalseShows the grow box, the 15×15 resize handle at the bottom right. Dragging it updates width and height within the limits below and fires vf-resize
min-width, min-heightnumber80, 54Whole system px. The smallest size the grow box allows. Properties: minWidth, minHeight
max-width, max-heightnumber—Whole system px. The largest size the grow box allows. Make one equal to its min to lock that axis. These limit the grow box only, not width and height. Properties: maxWidth, maxHeight
variant'utility'—A utility window: a 12px dotted title bar and 7×7 close and zoom boxes. The title isn't shown, but heading still names the boxes. Floats above document windows in a vf-desktop
scrollbars'vertical' | 'horizontal' | 'both'—Scrollbars on the window frame. On a resizable window the grow box sits in the corner, even with one scrollbar. With a status bar, the grow box moves into it and the scrollbar runs down to it
header-heightnumber—Whole system px. The header's height, including its rule. Unset, it fits its content. Property: headerHeight
placementAt(x, y)method—Converts a viewport point (CSS px) to { left, top } in the body, in system px, scroll offset included. Write the result to a child dropped there
measure()method—Re-measures the scroll area. Changes to content, slots and top/left are already tracked. Does nothing without scrollbars
show({ from })method—Clears hidden. With from, a box in viewport CSS px such as an icon's cellRect(), the window zooms open from that box and appears about 300 ms later. Until then it's invisible but focusable and clickable. A press or Escape skips to the end. Resolves true if it animated, false if it showed at once (no from, reduced motion, or no vf-desktop). It doesn't bring the window forward; call the desktop's bringToFront()
hide({ to })method—Sets hidden. With to, a box like from, the window disappears and zooms closed into that box. A press or Escape skips to the end; removing the window doesn't stop it. Resolves true if it animated, false if it hid at once (no to, reduced motion, no vf-desktop, already hidden, or still opening, in which case that show() also resolves false)
Anatomy
KindNameNotes
Slot(default)The body. It has no padding, so add your own, such as vf-stack pad. A placed child's (0,0) is the body's top-left corner
SlotheaderA full-width strip between the title bar and the body, with a 1px rule underneath and no padding. The vertical scrollbar starts below it. Takes no space when empty
SlotstatusA 15px status bar along the bottom, with a 1px rule above it and the grow box at its right end. Takes no space when empty
RolegroupNamed by its heading
StateopeningWhile show({ from }) animates and the window is still invisible. Select it with vf-window:state(opening)
Eventvf-closeThe close box was clicked. The window stays open until you hide or remove it
Eventvf-zoomThe zoom box was clicked
Eventvf-resize{ width, height, commit } in system px. Fires for each new size during a grow-box drag, after layout, then once with commit: true when the drag ends. Setting the size from code fires 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-offsetThe 18px and 12px title bars, the 15px status bar, the space between the title and the boxes, the utility bar's dots, the shadow

vf-dialog

A modal dialog, built on the native <dialog>. It opens in the top layer, traps focus and closes on Escape. With light-dismiss, a click outside also closes it. By default it has a striped title bar and can be dragged. frame="plain" has no title bar and stays put. Both use the same double border. The backdrop is transparent. top and left are viewport coordinates; leave them unset to center the dialog. Focus starts in the first text field, or on the default button. Return presses the default button.

Properties
PropertyTypeDefaultNotes
openbooleanfalseReflected. Stays in sync with the native dialog
widthnumber—Whole system px. Set it along with height. Unset, it's 260, logs a warning, and reflows when dragged near the viewport edge
heightnumber—Whole system px. Taller content is clipped. Unset, the dialog fits its content and logs a warning
headingstring''The title and the accessible name. With frame="plain", only the name
labelstring''Accessible name. Overrides heading. With neither set, the name is 'Dialog'
closablebooleanfalseShows a close box. Ignored with frame="plain"
light-dismissbooleanfalseA click outside closes the dialog, with reason: 'outside'. Both the press and the release must be outside, so a drag that ends outside doesn't close it. The click doesn't reach the page. Can be set while open. Property: lightDismiss, reflected
frame'plain'—No title bar, and it can't be dragged. Use it for alerts (see the last example). Content starts 5px inside the frame, or 20px from the top below a title bar
Anatomy
KindNameNotes
Methodshow()Opens it with the native showModal()
Methodclose()Closes it and fires vf-close with reason: 'close'
Eventvf-close{ reason: 'escape' | 'close' | 'outside' }, however it closed. 'outside' only with light-dismiss
KeyboardInitial focusA control with autofocus, else the first text field (vf-text-field, vf-number-field, vf-text-area, or a native one), else the default button
KeyboardReturn / EnterPresses the default button from anywhere in the dialog, even when Cancel has focus. Space presses the focused button. Links keep Enter. In a text area, Return adds a newline and keypad Enter presses the button. Escape closes
Slots(default)The content. top and left measure from the inside of the frame
Partsframe, title-bar, title, close-box, body, contenttitle-bar and close-box only on the default frame

To build an alert, use the plain frame with a row vf-stack holding a 32×32 icon and the message in the display face, then a vf-button-group, each placed with top and left. Set label, since there's no title bar to name it.

vf-separator

A 1px black line. In a menu it's gray and dotted. It has no width of its own, so give it one.

Properties
NameTypeDefaultNotes
verticalbooleanfalse1px wide. It takes its height from the parent, such as a stretching flex row
--vf-separator-colorToken--vf-blackGray in menus
--vf-separator-styleTokensoliddotted in menus
Role separator. No slots, parts or events.

vf-button

A push button with stepped pixel corners. Pressing inverts it. Keyboard focus draws a dashed line under the label.

Properties
PropertyTypeDefaultNotes
variant'default'—The default button, with a thick outer ring. The ring takes 4px of margin, so put buttons that share a row in a vf-button-group
disabledbooleanfalseDims the label. The border stays black, but a default button's ring dims
type'button' | 'submit' | 'reset''button'Form-associated: submit calls requestSubmit(), reset calls reset()
name, valuestring''Standard. Sent with the form when this button submits it. event.submitter is a button inside it, so use event.submitter.closest('vf-button')
hrefstring—Makes it a link: an <a href> that looks the same. type, name, value and the form* attributes are ignored, and Enter in a form field won't activate it. As a dialog's default button, Return follows it. When disabled, it drops the href and gets role="link" and aria-disabled="true"
target, rel, downloadstring—With href, as on <a>
Anatomy
KindNameNotes
Slot(default)The label
PartbuttonThe inner <button>, or <a> with href. Set min-width here
Events—Native click
Rolenative <button>Enter and Space press it. With href it's a native link, and Enter follows it
Tokens--vf-button-height (20px), --vf-button-ring-margin, --vf-button-flexvf-button-group sets the last two. They do nothing on their own

vf-button-group

A row of buttons, all as wide as the widest, lined up by their faces. A plain flex row lines up the default button by its ring instead. Compare the first two examples.

Properties
NameTypeDefaultNotes
verticalbooleanfalseStack the buttons in a column
naturalbooleanfalseEach button keeps its own width
--vf-button-group-gapToken12pxGap between buttons
Slot: default (vf-button elements). No parts or events. The group is as big as its buttons; position it from the parent.

vf-swatch

A color swatch button. With no color it shows a checkerboard, which also shows through translucent colors. shadow adds a drop shadow.

Properties
PropertyTypeDefaultNotes
colorstring (CSS color)—The fill. Unset shows the checkerboard
width, heightnumber24, 18Whole system px, border included
shadowbooleanfalseAdds a drop shadow
labelstring''Accessible name. Defaults to the color, or "transparent"
disabledbooleanfalseBlocks interaction. Nothing dims
Anatomy
KindNameNotes
Partsbutton, fillNo slots
Events—Native click. Not form-associated
Rolenative <button>Enter and Space press it
Token--vf-swatch-checkerThe checkerboard tile. --vf-shadow-offset sets the shadow depth

vf-checkbox

A 13×13 checkbox with a corner-to-corner ✕. Pressing thickens the border to 2px.

Properties
NameTypeDefaultNotes
checkedbooleanfalseReflected
disabled, name——Standard, form-associated. disabled dims the label, removes the tab stop, and follows a <fieldset disabled> ancestor
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 radio button. The group is the form control: it holds the value and name, and moves focus with a roving tabindex. A vf-radio outside a group works on its own.

vf-radio-group
NameTypeDefaultNotes
valuestring''The selected radio's value. Set it to select that radio
name, disabled——Standard, form-associated
labelstring''Accessible name (or use vf-label for / aria-labelledby)
vf-changeEvent—{ value }
Slot: default (the radios, at any depth). Role radiogroup; arrow keys move and select. No parts.
vf-radio
NameTypeDefaultNotes
valuestring''The group's value when this radio is selected
checkedbooleanfalseReflected. In a group, the group's value decides
disabledbooleanfalseDims the label only
vf-changeEvent—{ value }, on user interaction only
Slot: default (the label). Parts: circle, label. Not form-associated; the group is.

vf-text-field

A 22px-tall text input in the display face. Selected text is inverted.

Properties
NameTypeDefaultNotes
value, placeholder, name, disabled, readonly, type——Standard, form-associated. type (default 'text') is passed to the inner input
labelstring''Accessible name. If empty, an aria-label, aria-labelledby or <label for> on the host is used instead
requiredbooleanfalseNative constraint validation: reportValidity() shows the browser's message, :invalid matches the host, and the form won't submit
descriptionstring''Hint text for assistive tech. If empty, an aria-describedby on the host is used. A validation message is added to 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. Token: --vf-control-height (22px). The focus line draws 2 system px below the field, so a tight overflow: hidden parent clips it.

vf-text-area

A multi-line text field. It takes everything vf-text-field does, plus rows. The scrollbar is always there, blank 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 number field with up and down arrows. The input is 22 system px tall like the other fields; the 25px arrows beside it set the element's height.

Properties
NameTypeDefaultNotes
min, maxnumber—Clamp the value. Also set aria-valuemin/max
stepnumber1Step size, and the value's decimal precision
value, placeholder, name, disabled, readonly, label——Standard. readonly also disables the arrows
vf-input / vf-changeEvent—{ value, valueAsNumber }, on every keystroke / on commit or a step
--vf-number-field-widthToken4emWidth of the input
Parts: input, stepper. Role spinbutton on the input; ↑/↓ step, Home/End jump to min/max.

vf-select & vf-option

A popup menu. Press, drag to an item and release; or click to open it and click an item. The chosen item blinks, then the menu closes.

The menu opens with the selected item over the control. The control is as wide as its widest option; set min-width to make it wider.

size="small" uses the body face, a 12px control and 10px rows.

no-shadow removes the control's 1px shadow. The open menu keeps its shadow.

A menu taller than the screen is cut off, with an arrow at the edge where items are hidden. Rest the pointer on the arrow to scroll a row at a time. To keep long menus clear of a menu bar, set --vf-popup-inset-top on :root (24px for a vf-menu-bar).

vf-select
NameTypeDefaultNotes
valuestring''The selected option's value
name, disabled——Standard, form-associated. disabled dims the label only
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 menus), --vf-popup-inset-top / --vf-popup-inset-bottom (4px, the gap a long menu keeps from the screen edge; set on :root). Role combobox with a listbox. Space, Enter or ↓ opens; arrows move; Enter or Space selects; Esc cancels; Home/End jump. The menu is position: fixed, so overflow: hidden parents don't clip it.
vf-option
NameTypeDefaultNotes
valuestring''What the select reports
disabledbooleanfalseDimmed, and can't be picked
selectedbooleanfalseReflected. Set by the select
activebooleanfalseThe highlight during a drag. Set by the select
Slot: default (the label). Part: check. Role option.

vf-progress-bar

A progress bar: a black fill from the left, or moving diagonal stripes when indeterminate. Give it a width.

Properties
NameTypeDefaultNotes
valuenumber00–max
maxnumber100
indeterminatebooleanfalseMoving stripes, and no aria-valuenow
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.

vf-slider

A horizontal slider. System 7 had none; this one adapts the later Mac OS slider to 1-bit. Give it a width.

Properties
NameTypeDefaultNotes
valuenumber0
min, max, stepnumber0, 100, 1Standard. Arrow keys move by step, Page Up/Down by max(step, range/10)
name, disabled——Standard, form-associated. disabled dims the whole slider
labelstring''Accessible name
vf-input / vf-changeEvent—{ value }, on every move / on commit
Parts: track, rail, thumb. Role slider.

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

Pull-down menus. Press a title, drag to a command and release; or click a title, then click a command. Dragging across the bar switches menus. Releasing over a disabled item, a separator or outside the menu chooses nothing. The chosen item blinks (not under prefers-reduced-motion), then vf-menu-select fires.

A vf-menu also works on its own:

vf-menu-bar
KindNameNotes
PropertiesroundedBoolean. Rounds the bar's top corners like a screen's
PropertiesshortcutsBoolean. Makes every item's shortcut work from the keyboard. Shortcuts are page-wide, so set it on one menu bar only
Slot(default)vf-menu elements. One opens at a time. While one is open, hovering another title switches to it, and ←/→ move between menus
PartbarThe bar. Pad it to line the menus up with your content column
Token--vf-menubar-height20px, including the 1px rule
vf-menu
NameTypeDefaultNotes
labelstring''The title, and the accessible name when the label slot replaces it
openbooleanfalseReflected. Set by the bar, or by the menu itself when standalone
shortcutsbooleanfalseKeyboard shortcuts for a standalone menu. In a bar, set it 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 contentSent in the event detail
shortcutstring''The shortcut shown on the right (⌘N), exposed as aria-keyshortcuts. With shortcuts set on the menu or bar, pressing it runs the item from anywhere and flashes the menu's title. Disabled items ignore it. A matched key is preventDefault()ed. A bare letter never matches
checkedbooleanfalseShows a ✓
checkablebooleanfalseA toggle that starts unchecked. Role menuitemcheckbox
disabledbooleanfalseDims the whole row, ✓ included
activebooleanfalseThe highlight during a drag. Set by the menu
activate()Method—Blinks, then selects, as if chosen with the mouse
vf-menu-selectEvent—{ value, item }
Slot: default (the label). Parts: item, check, label, shortcut. Tokens: --vf-menu-row-height (16px), --vf-menu-shortcut-column (23px), --vf-select-gutter.

vf-list & vf-list-item

A list box with type-ahead and an always-visible scrollbar. Each row can take an icon 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
disabledbooleanfalseDisables every row. Re-enabling leaves rows disabled that were disabled on their own
labelstring''Accessible name
vf-changeEvent—{ value, values }
--vf-list-max-heightToken200pxHeight before it scrolls
Part: list. Roles listbox / option. Arrow keys move, Space toggles when multiple, and typing jumps to a match. Give the list a width.
vf-list-item
NameTypeDefaultNotes
valuestring''Reported by the list
selectedbooleanfalseReflected. The row inverts
disabledbooleanfalseSkipped by selection and type-ahead
Slots: default (the text), icon (ignored by type-ahead). Part: text, which truncates with an ellipsis. Keep row height minus icon height even, or the icon lands between pixels.

vf-scroll-area

A scrolling box with System 7 scrollbars. On the axes it reserves, the scrollbar is always there, blank until the content overflows. In an inactive window the scrollbars go blank. For scrollbars on a window's frame, use vf-window[scrollbars].

Properties
NameTypeDefaultNotes
axis'vertical' | 'horizontal' | 'both''vertical'Which scrollbars are always shown. The other axis gets one only when it overflows
cornerbooleanfalseKeeps the 15×15 corner square beside a single scrollbar, for a grow box. both always has one
labelstring''Accessible name for the scrolling viewport. When set, the viewport also gets role="region"
measure()method—Re-measures and updates the scrollbars. Changes to content, slots and top/left are already tracked; call this for anything else, like a transform
placementAt(x, y)method—Converts a viewport point (CSS px) to { left, top } in the scrolled content, in system px. Write the result to a child dropped there
Slot: default. Part: viewport, focusable and scrolled by the arrow and Page keys. Set a width and height. The content is at least as wide as the viewport, and widens for content that can't wrap. Sticky children work. Overscroll is off; to let the wheel carry on to the page, set ::part(viewport) { overscroll-behavior: auto }. The scrollbars look the same in every browser.

vf-fieldset

A group box with its legend in the top border. The legend's background is --vf-surface, white by default. On a colored background, set it to match.

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

vf-grid

A grid of equal cells with 1px lines between them, for tool palettes, swatch tables and pattern pickers. The lines can be solid or dashed, and draw behind the cells.

Each item is centered in its cell. place-self: stretch fills the cell instead, and is the default with collapse. The grid has no role, keyboard handling or selection; add a role such as group or radiogroup yourself.

Properties
NameTypeDefaultNotes
columnsnumber1Cells across
rowsnumber—Cells down. Unset, it fits the items. Set, empty cells still get their lines
cell-width, cell-heightnumber16Whole system px. Properties: cellWidth, cellHeight
rules'solid' | 'dashed' | 'none''solid'The line style. none removes the lines, so cells touch
framelessbooleanfalseNo outer border
collapsebooleanfalseLike border-collapse: collapse. An item's own 1px border merges with the grid line
Slot: default (the items, in order). Parts: grid, rules. Tokens: --vf-black, --vf-surface. No events.

vf-stack

A flexbox with gap, pad, width and height in system px. It's one of two ways to lay out components without a stylesheet. The other is top and left, below.

Your own CSS can't size in system px. Each component sets --vf-scale on itself, not on the document, so var(--vf-scale, 1) outside a component is just 1.

A column is as wide as its widest child, and a row as tall as its tallest. Children keep their natural size. The stack draws nothing, has no role, and lets font, line height, color and text selection inherit.

Properties
NameTypeDefaultNotes
direction'column' | 'row''column'The main axis
gapnumber0Whole system px between children
padstring | number—Padding in whole system px, one to four values as in CSS (pad="12", pad="10 12", pad="14 12 10"). Use it to pad content in a vf-window or vf-scroll-area, which have no padding
place'start' | 'center' | 'end'per directionWhere children sit across the stack. Defaults to start in a column and center in a row. Centering rounds toward the start, to stay on whole system px
width, heightnumber—Whole system px, optional. In a row, set a width to give fill-width children space to fill
fill-width, fill-heightbooleanfalseOn a child: fill the stack's width (or height). fill-width always works in a column but needs a set width in a row; fill-height is the reverse. Children filling along the main axis share the space equally. A stack can use them on itself too, inside a parent that isn't a stack
Slot: default. Children don't grow or shrink (flex: 0 0 auto). Your own styles on a child, such as align-self: stretch, win. No parts, tokens, events or role.

Placing with top and left

Every component takes top and left in system px. Set either one and the element is absolutely positioned, with the other defaulting to 0. Set neither and it stays in flow. Positions are relative to the nearest positioned ancestor. Every kit container is one; your own element needs position: relative, or use a vf-container.

That includes vf-option, vf-menu-item, vf-list-item and a menu bar's vf-menu. A placed row leaves the flow: it no longer counts toward the menu's width, and the rows after it close the gap.

On vf-dialog, top and left are viewport coordinates, and unset means centered. Dragging updates top and left, so read a moved element's position from el.left, not style.left, which holds a calc().

fixed pins an element to the visible area of the nearest scroller, so the content scrolls under it. On its own, fixed puts the element at (0,0). A fixed element must come first in its parent, and it draws over placed siblings. Where nothing scrolls, it behaves like any placed element.

origin sets which point of the element top and left position: top left (the default), top center, top right, center left, center, center right, bottom left, bottom center or bottom right. The point stays on whole system px as the element resizes, rounding toward the start. placementAt(x, y, child) takes the child's origin into account. The Page Setup dialog in vf-dialog uses it to center its title on left="145" and to pin its buttons' bottom-right corner at (274, 168).

vf-container

A plain box with a width and height in system px. It draws nothing unless you set pattern or rule. Children flow normally, or position themselves with top and left.

Properties
NameTypeDefaultNotes
width, heightnumber—Whole system px. Content that doesn't fit overflows. An unset axis fits the content
patternstring—A pattern name (bricks, gray-50, …, the 38 shown above) or 16 hex digits for a custom 8×8 pattern: one byte per row, high bit on the left, 1 for black. Drawn as the background, black on --vf-white, from the top-left corner. An unknown value draws nothing and logs a warning. In forced-colors mode a plain background replaces it
rulestring—Edges to draw a 1px --vf-black line on, separated by spaces (bottom, top bottom, …). The line is inside the box, so a 24px strip with rule="bottom" has 23px above the line. Children start inside the lines. An invalid value draws nothing and logs a warning
fill-width, fill-heightbooleanfalseAs in vf-stack. On a child, fill this box (fill-height needs a set height). On the container, fill its parent
Slot: default. The container can be placed with top and left too. It keeps itself on the device-pixel grid, and everything inside moves with it, kit or not. Lets text styles inherit, like vf-stack. No parts, tokens, events or role.

vf-label

A text label, like "Name:" beside a field. It uses the display face on a 16px line by default.

for works like <label for>: clicking the label focuses the target, and the label becomes its accessible name, unless you've named it already.

Properties
NameTypeDefaultNotes
forstring—The control's id, in the same document or shadow root. A disabled control isn't focused. The control can be defined later
face'display' | 'body''display'The typeface
dimbooleanfalseGrays the text (--vf-disabled)
widthnumber—Whole system px. Give a column of labels one width to line up their fields. Longer text overflows
Slot: default. Part: label. Line height: --vf-line-height-display (16px) or --vf-line-height (12px), by face; --vf-label-line-height overrides both for labels only. display: inline-block.

vf-paragraph

A paragraph in the body face on a 12px line, or 16px in the display face. It has no margins; space paragraphs yourself.

Properties
NameTypeDefaultNotes
face'display' | 'body''body'
dimbooleanfalse
Slot: default. Part: paragraph. Line height: --vf-line-height (12px) or --vf-line-height-display (16px), by face; --vf-paragraph-line-height overrides both for paragraphs only. The text is selectable.

vf-img

Pixel art. It wraps an <img> and draws one image pixel per system px, scaled with nearest-neighbor. The <img> stays in your light DOM, so alt, srcset and loading work as usual.

Properties
NameTypeDefaultNotes
width, heightnumberthe image's natural sizeWhole system px. Set them to reserve space before the image loads; otherwise the box is 0×0 until it does
Slot: default (one <img>; use alt="" when it's decorative). Part: frame.

vf-icon

A Finder icon: a picture and a name that select, move and rename together.

Put the 32×32 art in the large slot and the 16×16 art in small; size picks which one shows. The art area has a fixed size, so icons in a row line up. Group icons in a vf-icon-field.

color marks the art as a color icon: selecting it darkens the art instead of inverting it. The name inverts either way.

open shows that the icon's window is open: the outline stays and the inside fills with dots. It's drawn from the art you slotted, so it needs no extra file, and cross-origin images work.

Names are never shortened or wrapped. width sets the cell's width and must be even; a longer name overflows it, centered.

The drag outline draws over everything on the desktop, menu bar included. A drag fires vf-drag-start, vf-drag as it moves, and a cancelable vf-drop whose default action moves the icon. What a drop means is up to the page: to file an icon into a folder, cancel the drop and move it yourself. target highlights the folder under the pointer, and the destination's placementAt() converts the drop point into its coordinates.

Properties
NameTypeDefaultNotes
labelstring''The name. Empty shows no name plate
sizelarge | smalllargeWhich art shows, 32×32 or 16×16. A vf-icon-field's size sets it on every icon inside
selectablebooleanfalseClick to select. Also makes the icon focusable. movable and editable need it
selectedbooleanfalseReflected. The art inverts, and the name takes the --vf-highlight colors
colorbooleanfalseA color icon. Selecting it darkens the art, each color halfway to black, instead of inverting it
openbooleanfalseShows the icon as open: the outline stays and the inside fills with dots. Drawn from the slotted art
targetbooleanfalseReflected. Highlights the icon as a drop target. The art looks selected, but the selection, its events and aria-selected don't change
movablebooleanfalseDrag to move, or use the arrow keys (Shift for 8px). Escape cancels a drag. Fires the drag events below
editablebooleanfalseClick the name of a selected icon to rename it. The field opens after the double-click delay, so a double-click opens the icon instead
widthnumberthe wider of cell and nameEven system px. The cell's width; a longer name overflows it
maxlengthnumber31The longest name the rename field accepts. A label you set isn't truncated
setSelected(next)method—Selects or deselects like a click, firing vf-select. Setting selected fires nothing
touches(box)method—Whether a viewport box overlaps the art cell or the name. The selection rectangle uses it
cellRect()method—The art cell's box in viewport CSS px (32×32 system px, or 16×16 when small), or null if the icon isn't rendered. Hand it to a window: win.show({ from: icon.cellRect() }), win.hide({ to: icon.cellRect() })
moveTo(left, top)method—Moves the icon like a drop: kept inside its container, or only off the top and left edges in a scroller. Setting left and top directly isn't limited
dragTo(left, top)method—Slides a dotted outline to the new position, then moves the icon there like moveTo. Resolves true if the outline moved, false if the icon moved at once (already there, or reduced motion). Fires no drag events and doesn't need movable. A press or Escape finishes it at once
vf-selectEvent—{ selected }
vf-changeEvent—{ label, previous }: a rename was saved
vf-openEvent—{}: a double-click anywhere on the icon, a double tap with a finger or pen (within 500ms and 24 CSS px), or ⌘O / ⌘↓ (Ctrl outside macOS). Return renames instead
vf-name-too-longEvent—{ attempted, accepted, limit }: a rename went past maxlength
vf-name-rejectedEvent—{ attempted, kept, reason }: a rename was empty. The old name stays, and no vf-change fires
vf-drag-startEvent—{ left, top, icons }, once the pointer moves 4 CSS px from the press (10 for a finger or pen). left and top are the icon's position. icons lists every dragged icon, this one first; dragging a selected icon brings the other selected ones. Less movement is a click and fires nothing
vf-dragEvent—{ clientX, clientY, left, top, x, y, icons }, as the drag moves: the pointer in CSS px, the proposed position in the container in system px (before limits), the outline's corner in viewport CSS px for dropping into another container, and the dragged icons
vf-dropEvent—On release, with the same detail. Cancelable. The default action moves every dragged icon by the same amount, kept inside the container as a group. preventDefault() moves nothing, and it's safe to move the icons to another parent in the handler
vf-drag-cancelEvent—{}: Escape or a pointercancel ended the drag, and nothing moved
--vf-icon-gapToken0pxSpace between the art and the name
--vf-icon-label-heightToken12pxThe name's line height
Slots: large (32×32 art, usually a vf-img), small (16×16). Parts: frame, icon, label, plate, input. Both images load even though one shows; slot only the size you use, or use data URIs. In a vf-icon-field a selectable icon is an option with aria-selected. Outside one it's role="img", named by its label.

vf-icon-field

Holds a set of icons, such as the desktop's or a folder window's. It's a multi-select listbox named by label. It draws nothing. Unless you give it a size or fill-width / fill-height, it takes no space, and placed icons position against the desktop or window as if it weren't there.

Properties
NameTypeDefaultNotes
labelstring''Accessible name. Your own role or aria-* attributes override it
sizelarge | small—Sets size on every icon inside, including ones added later. Unset, icons keep their own
top, left, width, heightnumber—System px. When set, the field is a box and its icons position against it
fill-width, fill-heightbare attribute—Fill the parent, such as the desktop or a window body, to give the field a background you can drag a selection rectangle on. Icons still position against the parent
dragIcons(moves)method—{ icon, left, top }[]. Moves the icons one at a time, in array order, each with a sliding outline. Resolves when the last one lands. A second call, a press or Escape finishes it at once. Under reduced motion every icon moves at once
Slot: default (the icons). Always aria-multiselectable. vf-desktop has no field of its own: add one beside the windows, and one in each folder window's body. Dragging on the background of a filled or sized field draws a selection rectangle, and each change fires vf-select. With Shift or ⌘, the rectangle toggles icons against the current selection. Escape cancels. The rectangle draws under the windows on the desktop, or inside the window.

Focus

Every component can take focus, works from the keyboard and has an accessible name, like the native element it replaces. Full keyboard access came after System 7, so the focus mark is the kit's own: a 1px dashed line under the focused control. It shows for keyboard focus only, never after a click.