Skip to content

Elements

phil/ux-css

The base layer, phil.base, dresses the HTML elements without a class.

ElementsWhat they get
*, ::before, ::afterbox-sizing: border-box
htmlThe sans-serif stack, the body line height, the text and surface colours, the brand as accent-color
bodyNo margin, the body size
h1 to h6The text scale from 3xl down to sm, the tight line height, balanced lines
p, ul, ol, dl, blockquote, pre, figure, tableA bottom margin of md; p with pretty wrapping
hgroupIts children without margins: an eyebrow and its heading read as one
aThe link colour, an underline drawn from the font, thicker on hover
:focus-visibleA ring of --phil-color-focus, 3:1 on the surface
img, svg, video, canvas, pictureBlocks, never wider than their container
figcaption, smallThe small size; captions muted
hrA rule of the border colour
code, kbd, samp, preThe monospace stack; pre scrolls on the raised surface
blockquoteA brand rule on its start side
markThe warning tint
dt, ddA medium term, its description flush with it
kbd, abbr[title]A key on the raised surface; a dotted underline
[hidden]Hidden, whatever display a class sets; until-found is left to the browser

Forms

ElementsWhat they get
fieldset, legendA rounded frame, a medium title
labelA block over its control; beside it, at touch-target height, when it wraps a check box or a radio
Text inputs, date, search and the like, select, textareaFull width, the height of a touch target (2.75rem), the loud neutral border, 3:1 on the surface
::placeholderThe muted text colour
checkbox, radio, range, date, the pickersThe browser's own controls, in the brand through accent-color and the scheme through color-scheme; a check box or a radio is 24 CSS pixels
color, fileA framed swatch; ::file-selector-button as a quiet button
:user-validThe success border
:user-invalid, [aria-invalid="true"]The danger border, doubled so that it does not lean on colour alone
:disabledFaded, a not-allowed cursor

:user-valid and :user-invalid wait for the reader to touch the field, so a blank form opens without errors.

Symfony's forms

The markup of form_row is dressed as Symfony writes it, with no form theme to install.

MarkupWhat it gets
form div holding a label or a buttonThe space between two rows
label.requiredA star, hidden from screen readers, since the field carries required
The errors, a plain ul after the label or first in the formNo bullets, small, in the danger colour
.help-textSmall and muted, under the widget
A check box row, label firstThe box before its label, without touching the order of the source
Expanded choices, siblings in a divA box and its label on each line

Buttons

button, input of type submit, button or reset, and [role="button"]: solid and in the brand, 2.75rem high, faded when disabled or aria-disabled. Hovering or pressing mixes the fill towards the text colour. See Components for the other looks.

Tables

ElementsWhat they get
tableFull width, collapsed, tabular figures
captionSmall, muted, at the start
th, tdStart-aligned, a line under each row
thead th, tfootA heavy line of the loud neutral, a medium weight
figure:has(> table)Scrolls a table too wide for the page

Striped rows are the class isStriped.

Dialogs and popovers

ElementsWhat they get
dialogThe --phil-panel-md width or --phil-dialog-size, at most the height of the screen, scrolling inside; modal or not
dialog > footerIts actions at the end of the line
dialog::backdrop--phil-color-backdrop
html:has(dialog:modal)Holds the page still behind a modal
[popover]A panel, centred by the browser until a controller places it and sets data-placed

They fade in and out where the browser can animate the top layer.

Disclosures, navigation and gauges

ElementsWhat they get
details, summaryA summary tall enough to touch; the content opens to height: auto where the browser allows it
nav listsNo bullets, in a row that wraps
nav aThe height of a touch target
nav a[aria-current]The text colour and a bold weight
progressA rounded bar in the brand; an indeterminate one travels, and stands still under reduced motion
meterThe same, success, warning or danger as the browser judges the value

Every direction is a logical property, so a right-to-left page needs nothing more. The only physical axis is the scrolling of tables, tab bars, code and carousels, written overflow-x because overflow-inline is not yet in every browser this stylesheet supports.

Under prefers-reduced-motion: reduce, or below a root that carries data-motion="reduce", every animation and transition ends at once. These declarations are !important in the base layer, which makes them win over every later layer and over the site's own styles.

The reader's display preferences

The page follows the system's media queries. An application that lets each reader choose writes the choice on the root element, and the base layer applies it as it applies the query; without the attribute, the query decides.

Attribute on the rootSame asEffect
data-theme="light" or "dark"prefers-color-schemeThe scheme, see Themes
data-motion="reduce"prefers-reduced-motion: reduceEvery animation and transition ends at once
data-contrast="more"prefers-contrast: moreMuted text takes the text colour, borders the loud neutral border
data-text-size="large" or "larger"the browser's text sizeThe root at 112.5 % or 125 %, so every size of the scale follows
<html lang="{{ app.request.locale }}" data-motion="reduce" data-text-size="large">