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.
| Kind | Name | Notes |
|---|---|---|
| Property | width, height | The screen size in system px (default 512×342). The element adds the bezel on every side |
| Property | bezel | A 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 |
| Property | pattern | A 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 |
| Property | activeWindow | Read-only. The active document window, or null. Never a utility window |
| Method | fitWithin(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 |
| Method | bringToFront(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 |
| Method | clearActive() | Deactivates every document window until the next press, focus, added window or bringToFront(). Utility windows stay active |
| Method | placementAt(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 |
| Event | vf-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 |
| Part | desktop | The screen |
| Token | --vf-desktop | The color under the pattern (#808080) |
| Token | --vf-desktop-pattern | The pattern, as a background-image |
vf-window
A window. The five classic Mac window types are each one line of markup.
| Type | Markup |
|---|---|
| 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. Set it along with height. Unset, the window falls back to block layout and logs a warning |
height | number | — | Whole system px. Taller content is clipped; use scrollbars to reach it. Unset, the body fits its content and logs a warning |
heading | string | '' | The title. Also used in the close and zoom boxes' accessible names (Close Read Me) |
active | boolean | true | Draws 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 |
closable | boolean | true | Shows the close box |
zoomable | boolean | false | Shows the zoom box |
movable | boolean | false | Drag the title bar to move it. The drag updates top and left |
outline-drag | boolean | false | Drag 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 |
resizable | boolean | false | Shows 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-height | number | 80, 54 | Whole system px. The smallest size the grow box allows. Properties: minWidth, minHeight |
max-width, max-height | number | — | 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-height | number | — | 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) |
| Kind | Name | Notes |
|---|---|---|
| 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 |
| Slot | header | A 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 |
| Slot | status | A 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 |
| Role | group | Named by its heading |
| State | opening | While show({ from }) animates and the window is still invisible. Select it with vf-window:state(opening) |
| Event | vf-close | The close box was clicked. The window stays open until you hide or remove it |
| Event | vf-zoom | The zoom box was clicked |
| Event | vf-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 |
| 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 | The 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.
| Property | Type | Default | Notes |
|---|---|---|---|
open | boolean | false | Reflected. Stays in sync with the native dialog |
width | number | — | Whole system px. Set it along with height. Unset, it's 260, logs a warning, and reflows when dragged near the viewport edge |
height | number | — | Whole system px. Taller content is clipped. Unset, the dialog fits its content and logs a warning |
heading | string | '' | The title and the accessible name. With frame="plain", only the name |
label | string | '' | Accessible name. Overrides heading. With neither set, the name is 'Dialog' |
closable | boolean | false | Shows a close box. Ignored with frame="plain" |
light-dismiss | boolean | false | A 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 |
| Kind | Name | Notes |
|---|---|---|
| Method | show() | Opens it with the native showModal() |
| Method | close() | Closes it and fires vf-close with reason: 'close' |
| Event | vf-close | { reason: 'escape' | 'close' | 'outside' }, however it closed. 'outside' only with light-dismiss |
| Keyboard | Initial focus | A 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 |
| Keyboard | Return / Enter | Presses 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 |
| Parts | frame, title-bar, title, close-box, body, content | title-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.
| Name | Type | Default | Notes |
|---|---|---|---|
vertical | boolean | false | 1px wide. It takes its height from the parent, such as a stretching flex row |
--vf-separator-color | Token | --vf-black | Gray in menus |
--vf-separator-style | Token | solid | dotted 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.
| Property | Type | Default | Notes |
|---|---|---|---|
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 |
disabled | boolean | false | Dims 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, value | string | '' | Standard. Sent with the form when this button submits it. event.submitter is a button inside it, so use event.submitter.closest('vf-button') |
href | string | — | 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, download | string | — | With href, as on <a> |
| Kind | Name | Notes |
|---|---|---|
| Slot | (default) | The label |
| Part | button | The inner <button>, or <a> with href. Set min-width here |
| Events | — | Native click |
| Role | native <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-flex | vf-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.
| Name | Type | Default | Notes |
|---|---|---|---|
vertical | boolean | false | Stack the buttons in a column |
natural | boolean | false | Each button keeps its own width |
--vf-button-group-gap | Token | 12px | Gap 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.
| Property | Type | Default | Notes |
|---|---|---|---|
color | string (CSS color) | — | The fill. Unset shows the checkerboard |
width, height | number | 24, 18 | Whole system px, border included |
shadow | boolean | false | Adds a drop shadow |
label | string | '' | Accessible name. Defaults to the color, or "transparent" |
disabled | boolean | false | Blocks interaction. Nothing dims |
| Kind | Name | Notes |
|---|---|---|
| Parts | button, fill | No slots |
| Events | — | Native click. Not form-associated |
| Role | native <button> | Enter and Space press it |
| Token | --vf-swatch-checker | The 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.
| Name | Type | Default | Notes |
|---|---|---|---|
checked | boolean | false | Reflected |
disabled, name | — | — | Standard, form-associated. disabled dims the label, removes the tab stop, and follows a <fieldset disabled> ancestor |
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 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.
| Name | Type | Default | Notes |
|---|---|---|---|
value | string | '' | The selected radio's value. Set it to select that radio |
name, disabled | — | — | Standard, form-associated |
label | string | '' | Accessible name (or use vf-label for / aria-labelledby) |
vf-change | Event | — | { value } |
Slot: default (the radios, at any depth). Role radiogroup; arrow keys move and select. No parts. | |||
| Name | Type | Default | Notes |
|---|---|---|---|
value | string | '' | The group's value when this radio is selected |
checked | boolean | false | Reflected. In a group, the group's value decides |
disabled | boolean | false | Dims the label only |
vf-change | Event | — | { 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.
| Name | Type | Default | Notes |
|---|---|---|---|
value, placeholder, name, disabled, readonly, type | — | — | Standard, form-associated. type (default 'text') is passed to the inner input |
label | string | '' | Accessible name. If empty, an aria-label, aria-labelledby or <label for> on the host is used instead |
required | boolean | false | Native constraint validation: reportValidity() shows the browser's message, :invalid matches the host, and the form won't submit |
description | string | '' | Hint text for assistive tech. If empty, an aria-describedby on the host is used. A validation message is added to 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. 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.
| 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 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.
| Name | Type | Default | Notes |
|---|---|---|---|
min, max | number | — | Clamp the value. Also set aria-valuemin/max |
step | number | 1 | Step size, and the value's decimal precision |
value, placeholder, name, disabled, readonly, label | — | — | Standard. readonly also disables the arrows |
vf-input / vf-change | Event | — | { value, valueAsNumber }, on every keystroke / on commit or a step |
--vf-number-field-width | Token | 4em | Width 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).
| Name | Type | Default | Notes |
|---|---|---|---|
value | string | '' | The selected option's value |
name, disabled | — | — | Standard, form-associated. disabled dims the label only |
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 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. | |||
| Name | Type | Default | Notes |
|---|---|---|---|
value | string | '' | What the select reports |
disabled | boolean | false | Dimmed, and can't be picked |
selected | boolean | false | Reflected. Set by the select |
active | boolean | false | The 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.
| Name | Type | Default | Notes |
|---|---|---|---|
value | number | 0 | 0–max |
max | number | 100 | |
indeterminate | boolean | false | Moving stripes, and no aria-valuenow |
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. | |||
vf-slider
A horizontal slider. System 7 had none; this one adapts the later Mac OS slider to 1-bit. Give it a width.
| Name | Type | Default | Notes |
|---|---|---|---|
value | number | 0 | |
min, max, step | number | 0, 100, 1 | Standard. Arrow keys move by step, Page Up/Down by max(step, range/10) |
name, disabled | — | — | Standard, form-associated. disabled dims the whole slider |
label | string | '' | Accessible name |
vf-input / vf-change | Event | — | { value }, on every move / on commit |
Parts: track, rail, thumb. Role slider. | |||
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.
| 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 |
disabled | boolean | false | Disables every row. Re-enabling leaves rows disabled that were disabled on their own |
label | string | '' | Accessible name |
vf-change | Event | — | { value, values } |
--vf-list-max-height | Token | 200px | Height 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. | |||
| Name | Type | Default | Notes |
|---|---|---|---|
value | string | '' | Reported by the list |
selected | boolean | false | Reflected. The row inverts |
disabled | boolean | false | Skipped 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].
| Name | Type | Default | Notes |
|---|---|---|---|
axis | 'vertical' | 'horizontal' | 'both' | 'vertical' | Which scrollbars are always shown. The other axis gets one only when it overflows |
corner | boolean | false | Keeps the 15×15 corner square beside a single scrollbar, for a grow box. both always has one |
label | string | '' | 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.
| Name | Type | Default | Notes |
|---|---|---|---|
legend | string | '' | 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.
| Name | Type | Default | Notes |
|---|---|---|---|
columns | number | 1 | Cells across |
rows | number | — | Cells down. Unset, it fits the items. Set, empty cells still get their lines |
cell-width, cell-height | number | 16 | Whole system px. Properties: cellWidth, cellHeight |
rules | 'solid' | 'dashed' | 'none' | 'solid' | The line style. none removes the lines, so cells touch |
frameless | boolean | false | No outer border |
collapse | boolean | false | Like 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.
| Name | Type | Default | Notes |
|---|---|---|---|
direction | 'column' | 'row' | 'column' | The main axis |
gap | number | 0 | Whole system px between children |
pad | string | 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 direction | Where 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, height | number | — | Whole system px, optional. In a row, set a width to give fill-width children space to fill |
fill-width, fill-height | boolean | false | On 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.
| Name | Type | Default | Notes |
|---|---|---|---|
width, height | number | — | Whole system px. Content that doesn't fit overflows. An unset axis fits the content |
pattern | string | — | 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 |
rule | string | — | 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-height | boolean | false | As 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.
| Name | Type | Default | Notes |
|---|---|---|---|
for | string | — | 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 |
dim | boolean | false | Grays the text (--vf-disabled) |
width | number | — | 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.
| Name | Type | Default | Notes |
|---|---|---|---|
face | 'display' | 'body' | 'body' | |
dim | boolean | false | |
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.
| Name | Type | Default | Notes |
|---|---|---|---|
width, height | number | the image's natural size | Whole 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.
| Name | Type | Default | Notes |
|---|---|---|---|
label | string | '' | The name. Empty shows no name plate |
size | large | small | large | Which art shows, 32×32 or 16×16. A vf-icon-field's size sets it on every icon inside |
selectable | boolean | false | Click to select. Also makes the icon focusable. movable and editable need it |
selected | boolean | false | Reflected. The art inverts, and the name takes the --vf-highlight colors |
color | boolean | false | A color icon. Selecting it darkens the art, each color halfway to black, instead of inverting it |
open | boolean | false | Shows the icon as open: the outline stays and the inside fills with dots. Drawn from the slotted art |
target | boolean | false | Reflected. Highlights the icon as a drop target. The art looks selected, but the selection, its events and aria-selected don't change |
movable | boolean | false | Drag to move, or use the arrow keys (Shift for 8px). Escape cancels a drag. Fires the drag events below |
editable | boolean | false | Click 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 |
width | number | the wider of cell and name | Even system px. The cell's width; a longer name overflows it |
maxlength | number | 31 | The 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-select | Event | — | { selected } |
vf-change | Event | — | { label, previous }: a rename was saved |
vf-open | Event | — | {}: 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-long | Event | — | { attempted, accepted, limit }: a rename went past maxlength |
vf-name-rejected | Event | — | { attempted, kept, reason }: a rename was empty. The old name stays, and no vf-change fires |
vf-drag-start | Event | — | { 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-drag | Event | — | { 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-drop | Event | — | 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-cancel | Event | — | {}: Escape or a pointercancel ended the drag, and nothing moved |
--vf-icon-gap | Token | 0px | Space between the art and the name |
--vf-icon-label-height | Token | 12px | The 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.
| Name | Type | Default | Notes |
|---|---|---|---|
label | string | '' | Accessible name. Your own role or aria-* attributes override it |
size | large | small | — | Sets size on every icon inside, including ones added later. Unset, icons keep their own |
top, left, width, height | number | — | System px. When set, the field is a box and its icons position against it |
fill-width, fill-height | bare 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.