Instruction file imported from miyako/HDI_UseCollections (
.github/instructions/css.instructions.md). Copyright stays with the author.
4D CSS Stylesheets — Agent Instructions
Overview
4D applications can use CSS stylesheets to control form object appearance, including macOS/Windows dark mode support via automatic color values, prefers-color-scheme media queries, and runtime color detection using hidden reference objects.
Automatic Color Values
4D provides special color keywords that adapt to the system color scheme automatically:
| Keyword | Applies to | Behaviour |
|---|---|---|
"automatic" |
stroke, fill |
Adapts text/foreground and background to light or dark mode |
"automaticAlternate" |
alternateFill |
Adapts the alternate row background in listboxes |
Rules
- Use
"automatic"for textstrokeand backgroundfillwhenever a fixed colour is not required. - Use
"automaticAlternate"for listbox alternate row fills instead of hardcoded near-white values like#F8FCFF. - When a column-level property is the same as the listbox-level property and there is only one column, remove the column-level override and define it at the listbox level only. Column-level properties are for overrides, not repetition.
- Replace hardcoded
#000000or#FFFFFF(or near-equivalents like#212121) instrokefor text and shape primitives with"automatic"only if you want them to adapt. If specific branded colours are needed, use CSS instead. - Form objects that omit
fillorstrokeentirely use a 4D-internal default, which is not the same as"automatic". To ensure they adapt to dark mode, explicitly set"fill": "automatic"and/or"stroke": "automatic". This is especially important for full-form background rectangles that rely on the implicit default fill.
⚠️ Scope: "automatic" is a form-object CSS property value, not an HTML/rich-text colour
"automatic" and "automaticAlternate" are only meaningful as the value of a fill/stroke/alternateFill property on a form object or CSS class (in form.4DForm, styleSheets*.css). They are not valid inside inline HTML/rich-text markup such as <span style="color:#000000"> embedded in JSON sample data, area/4D Write Pro content, or any other string that is rendered by an HTML/rich-text parser rather than 4D's own form-CSS engine.
A real mistake to avoid: a project-wide grep-and-replace for hardcoded hex colours accidentally rewrote color:#000000/color:#01168B inside <span style="color:..."> tags in Resources/*.json sample text (data consumed by a styled-text/rich-text area, not a form object) to color:automatic. This silently breaks rendering, since automatic is not a real CSS/HTML colour keyword outside 4D's own form styling. Before replacing any hardcoded colour, confirm the string is a .4DForm/CSS property value, not the content of a JSON/text/HTML field that merely happens to contain colour-looking hex codes.
CSS Stylesheets in 4D
File Loading
4D automatically loads these three stylesheets if they exist in /SOURCES/:
Project/Sources/styleSheets.css ← cross-platform
Project/Sources/styleSheets_mac.css ← macOS only
Project/Sources/styleSheets_windows.css ← Windows only
Dark mode rules are cross-platform, so they belong in styleSheets.css.
A form can also reference additional CSS files via its "css" property:
{ "css": ["myCustomStyles.css"] }
Syntax
4D CSS uses standard CSS selector syntax but the property names are 4D form object property names in camelCase, not HTML/CSS property names. Do not assume web CSS properties are valid in 4D.
Reference for property names:
- Form object properties: https://developer.4d.com/docs/FormObjects/propertiesReference
- CSS in 4D: https://developer.4d.com/docs/FormEditor/stylesheets
Common 4D CSS properties (not exhaustive):
| 4D CSS property | Purpose |
|---|---|
fill |
Background colour |
stroke |
Text/foreground colour |
alternateFill |
Alternate row background |
visibility |
"visible" or "hidden" |
borderStyle |
Border rendering style |
horizontalLineStroke |
Listbox horizontal grid line colour |
verticalLineStroke |
Listbox vertical grid line colour |
horizontalPadding |
Horizontal cell padding |
fontWeight |
"bold" or "normal" |
fontStyle |
"italic" or "normal" |
textAlign |
Text alignment |
fontSize |
Font size |
Media Queries
@media (prefers-color-scheme: light) {
.my-class {
fill: #F1F6FB;
stroke: #2C435A;
}
}
@media (prefers-color-scheme: dark) {
.my-class {
fill: #1e2d3d;
stroke: #B8D4E8;
}
}
Alpha Channel
4D does not support alpha channel in hex colours. Use only #RRGGBB (6 digits), not #RRGGBBAA.
Specificity Rules (Critical)
4D CSS follows standard CSS specificity rules with one critical addition:
Properties defined directly in the
.4DFormJSON have the highest specificity and will override CSS unless!importantis used.
Therefore, when moving a colour from a hardcoded form value to CSS:
- Remove the property from the
.4DFormJSON entirely - Add a
"class"property to the object in the.4DForm - Define the colour in CSS for that class
If you leave the hardcoded value in the form, CSS will not override it (unless you use !important, which is discouraged).
Assigning Classes
Add the "class" property to form objects in the .4DForm JSON:
{
"type": "text",
"class": "my-label",
"text": "Hello"
}
Multiple classes: "class": "label primary"
Runtime Color Detection (Hidden Reference Objects)
The Problem
4D has no API to directly query whether the current theme is dark or light in a boolean fashion. The commands:
Get application color scheme(returns"light","dark", or"inherited")FORM Get color scheme(returns"light","dark", or"inherited")
…may return "inherited", meaning the system theme is in use but you don't know which one it resolved to without additional logic.
The Workaround
Use hidden rectangle objects on the form as colour references. Their rendered colours are set by CSS media queries, and at runtime you read the actual resolved colour with OBJECT GET RGB COLORS.
Step 1: Add Hidden Rectangles to the Form
Place them on page 0 (shared/always loaded) so they are available during On Load:
"refMyColour": {
"type": "rectangle",
"top": 0,
"left": 0,
"width": 1,
"height": 1,
"class": "ref-my-colour",
"stroke": "transparent"
}
Step 2: Define Colours in CSS
@media (prefers-color-scheme: light) {
.ref-my-colour {
fill: #FFCCCC;
visibility: hidden;
}
}
@media (prefers-color-scheme: dark) {
.ref-my-colour {
fill: #5C2020;
visibility: hidden;
}
}
Note: visibility: hidden is set in both media queries to ensure the rectangle is always invisible regardless of scheme.
Step 3: Read at Runtime
var $fg; $bg : Integer
OBJECT GET RGB COLORS:C1074(*; "refMyColour"; $fg; $bg)
// $bg contains the resolved fill colour as a longint (0x00RRGGBB)
Step 4: Convert to Hex String
Use a helper method to convert the longint to #RRGGBB:
// RGBToHex method
#DECLARE($rgb : Integer)->$result : Text
var $r; $g; $b : Integer
$r:=($rgb >> 16) & 0x0000FF
$g:=($rgb >> 8) & 0x0000FF
$b:=$rgb & 0x0000FF
var $hex : Text
$hex:=""
var $i; $val : Integer
var $digits : Text
$digits:="0123456789ABCDEF"
For ($i; 1; 3)
Case of
: ($i=1)
$val:=$r
: ($i=2)
$val:=$g
: ($i=3)
$val:=$b
End case
$hex:=$hex+$digits[[$val\16+1]]+$digits[[$val%16+1]]
End for
$result:="#"+$hex
Use Case: Listbox Meta Expression
When a listbox uses "metaSource" to dynamically style rows/cells with fill colours, those colours are hardcoded strings. Use the hidden-rectangle technique to resolve theme-appropriate colours at form load time, then build the meta objects with those resolved values.
This is easy to miss with a form-JSON-only audit. A listbox's rowFillSource / rowStrokeSource / rowStyleSource properties (in .4DForm) just name an array or variable — the actual hardcoded colours live in .4dm method code (e.g. _FontBackground{$i}:=0x00FFFFFF), often in more than one file (an On Load initializer and a selection/refresh handler that has to stay in sync with it). A pure grep/scan of .4DForm for fill/stroke properties will never surface these. Always additionally:
grep -rn "rowFillSource\|rowStrokeSource\|rowStyleSource" Project/Sources/Forms/to find every listbox using a meta-expression.- For each hit, note the array/variable name(s) referenced.
grep -rnthat exact name across all.4dmfiles (form method, object methods) to find every assignment site — there is often more than one (initial load vs. a change/refresh event).- If any assignment hardcodes an RGB/hex literal, replace it with a value resolved from a hidden reference rectangle (see above), and make sure every assignment site uses the same resolved variable so they can't drift out of sync.
4D Method Token Reference
4D project mode source files may include token syntax (:Cnnn suffixes on commands). These tokens are optional — plain command names work correctly and 4D adds tokens automatically when it saves the file. Never invent or guess token numbers; an incorrect token silently resolves to the wrong command, causing hard-to-diagnose runtime errors. If unsure of a token, omit it entirely.
Mandatory verification step — no exceptions, even from apparent memory/confidence: before writing any :CNNN or :KNN:NN suffix, grep the actual project source files for that exact CommandName:CNNN (or ConstantName:KNN:NN) string.
- Match found → copy that verified token character-for-character.
- No match found → write the command/constant name with no token suffix. Do not substitute a token you "recall" being correct, one seen in another project, or one from general training knowledge — none of these are verified sources, and a confidently recalled wrong token is just as dangerous as a randomly invented one. Verifying one command's token does not license guessing another command's token in the same file or statement.
Known correct tokens (for reference only — omitting them is always safe):
| Command | Token | Notes |
|---|---|---|
OBJECT GET RGB COLORS |
:C1074 |
Reads foreground/background of a named object |
New object |
:C1471 |
Create a new object |
Form |
:C1466 |
Access the form data object |
Common mistake: :C382 is _O_REDRAW LIST (obsolete), not OBJECT GET RGB COLORS. This is exactly why guessing tokens is dangerous — always verify against existing project code or omit the token.
Command reference: https://developer.4d.com/docs/commands/
Listbox-Specific Guidelines
alternateFill
- Use
"automaticAlternate"at the listbox level for automatic dark/light adaptation. - Do not repeat the same
alternateFillat the column level unless it is a deliberate column-specific override. - Remove column-level
alternateFillif it matches the listbox-level value (it adds no value and creates maintenance burden).
Odd-row (fill) colour with light/dark variants
If you want a custom odd-row background (instead of fully automatic), do it via CSS classes and media queries:
- Assign a class to the listbox in
form.4DForm(for example,"class": "hdi-list"). - Remove JSON
fillfrom the listbox and its columns so CSS can apply (JSON wins on specificity). - Keep
alternateFillas"automaticAlternate"for even rows. - Define light/dark odd-row colours in
styleSheets.css:
@media (prefers-color-scheme: light) {
.hdi-list { fill: #FDFED3; }
}
@media (prefers-color-scheme: dark) {
.hdi-list { fill: #3A2F1F; }
}
Meta Source Colours
- Never hardcode light-mode-only colours in meta source methods.
- Use the hidden-rectangle reference technique described above.
- White text (
#FFFFFF) on a coloured background (e.g., red#FF4040) is acceptable in both modes because the cell fill provides contrast. - Do not stop at checking one assignment site. If a listbox's meta-source array is populated both on form load and on a selection-change/refresh event, both sites must resolve colours the same way (both via the hidden-rectangle technique, sharing the same resolved variables) — fixing only the load-time initializer and leaving a refresh handler with hardcoded hex reintroduces the bug the moment the user interacts with the listbox.
Colour Palette Recommendations
When choosing dark-mode equivalents, follow these principles:
| Light mode | Dark mode | Rationale |
|---|---|---|
Near-white backgrounds (#FFFFFF, #F8FCFF) |
Dark grey (#1E1E1E, #2A2A2A) |
Sufficient contrast without pure black |
Near-black text (#212121, #000000) |
Light grey (#E0E0E0) |
Readable on dark backgrounds |
Muted text (#696969) |
Lighter muted (#A0A0A0) |
Maintains hierarchy without being invisible |
Link blue (#1E90FF) |
Lighter blue (#5CB8FF) |
Accessible contrast on dark |
Alert red fill (#FF4040) |
Muted red (#CC3333) |
Less harsh on dark backgrounds |
Light pink fill (#FFCCCC) |
Dark red (#5C2020) |
Retains semantic meaning |
Light green fill (#B8EDB8) |
Dark green (#1E4D1E) |
Retains semantic meaning |
Panel grey (#C0C0C0) |
Dark panel (#3A3A3A) |
Structural differentiation |
Checklist for Dark Mode Migration
- Scan forms for hardcoded
strokeandfillcolours across every object type, not just shapes — atextobject'sfillis its background and is just as easy to leave hardcoded as a rectangle's; checkstrokeandfillindependently on each object since one can be"automatic"while the other is still a fixed hex value. Restrict this toform.4DForm/CSS property values, not colour-looking hex codes inside JSON/HTML/rich-text content fields (see scope note above). - Scan forms for objects that omit
fillorstrokeentirely — these use an internal default, not"automatic". Add"fill": "automatic"or"stroke": "automatic"explicitly so they adapt to dark mode. Pay special attention to full-form background rectangles. - Replace
#000000/#FFFFFFwith"automatic"where appropriate. - Replace hardcoded
alternateFillwith"automaticAlternate". - Remove column-level properties that duplicate listbox-level values.
- Move branded/specific colours from
.4DFormto CSS classes with media queries. - For listboxes with custom odd-row colour, set class-based
fillin light/dark CSS and keepalternateFill: "automaticAlternate". - Add hidden reference rectangles for any runtime colour logic (meta expressions, programmatic styling). Do not rely on a
.4DForm-only scan to find these:grep -rn "rowFillSource\|rowStrokeSource\|rowStyleSource"across all forms, thengrep -rneach referenced array/variable name across all.4dmfiles to find every assignment site (there is often more than one, e.g. load-time init plus a refresh/selection-change handler) and confirm none hardcode theme-specific RGB/hex. - Create
styleSheets.cssif it doesn't exist; define bothlightanddarkmedia query blocks. - Verify no inline
.4DFormproperty is overriding your CSS (specificity rule). - Test by toggling system appearance in System Preferences / Settings.
References
- Color scheme setting: https://developer.4d.com/docs/settings/interface#color-scheme
Get application color scheme: https://developer.4d.com/docs/commands/get-application-color-scheme- CSS in 4D: https://developer.4d.com/docs/FormEditor/stylesheets
- Form CSS property: https://developer.4d.com/docs/FormEditor/propertiesForm#css
- Properties reference (camelCase names): https://developer.4d.com/docs/FormObjects/propertiesReference