Skip to main content

CSS Property Mapping

CSS → Grida IR property mapping table and TODO tracker.

Status key: ✅ mapped | ⚠️ partial | 🔧 IR exists, not wired | ❌ IR missing | 🚫 out of scope

Import pipelines: HTML import (crates/grida/src/import/html/), SVG import (via usvg, crates/grida/src/import/svg/).


Box Model & Sizing​

CSS PropertyGrida IR FieldStatusNotes
widthLayoutDimensionStyle.layout_target_width✅px only; % not resolved
heightLayoutDimensionStyle.layout_target_height✅px only
min-widthLayoutDimensionStyle.layout_min_width✅
min-heightLayoutDimensionStyle.layout_min_height✅
max-widthLayoutDimensionStyle.layout_max_width✅
max-heightLayoutDimensionStyle.layout_max_height✅
padding (all sides)LayoutContainerStyle.layout_padding✅EdgeInsets (top/right/bottom/left)
margin(tree surgery)⚠️Fixed margins → wrapper+padding; auto/negative not supported; no collapse (see Known Limitations)
box-sizing--⚠️Assumed border-box; no explicit field
aspect-ratioLayoutDimensionStyle.layout_target_aspect_ratio🔧Field exists, CSS import not wired

Display & Layout​

CSS PropertyGrida IR FieldStatusNotes
display: flexLayoutContainerStyle.layout_mode = Flex✅
display: blocklayout_mode = Flex (column)⚠️Approximated as flex column
display: none(element skipped)✅Node not emitted
display: inline--❌Treated as block
display: grid--❌No grid in LayoutMode
flex-direction (row/column)layout_direction✅
flex-direction (row-reverse/column-reverse)layout_direction⚠️Reverse info lost
flex-wraplayout_wrap✅
flex-wrap: wrap-reverselayout_wrap⚠️Reverse info lost
align-itemslayout_cross_axis_alignment✅start/center/end/stretch
justify-contentlayout_main_axis_alignment✅start/center/end/between/around/evenly/stretch
flex-growLayoutChildStyle.layout_grow✅
flex-shrink--❌Not in LayoutChildStyle; Taffy hardcodes 0.0
flex-basis--❌Not in IR
align-self--❌Not in LayoutChildStyle
order--❌Not in IR
gap / row-gap / column-gapLayoutContainerStyle.layout_gap✅Direction-aware mapping
position: absoluteLayoutChildStyle.layout_positioning = Absolute✅
position: relativeLayoutChildStyle.layout_positioning = Auto✅
position: fixed/sticky--❌No viewport-relative positioning
top/right/bottom/left--❌No offset in LayoutChildStyle

Background & Paint​

CSS PropertyGrida IR FieldStatusNotes
background-colorPaint::Solid in fills✅hex/rgb/rgba/named
linear-gradient()Paint::LinearGradient✅
radial-gradient()Paint::RadialGradient✅
conic-gradient()Paint::SweepGradient✅
background-image: url()Paint::Image🔧IR exists, CSS import not wired
background-size--❌Not mapped
background-position--❌Not mapped
background-repeat--❌Not mapped
background-clip: text--❌No text-clip paint

Opacity & Blend​

CSS PropertyGrida IR FieldStatusNotes
opacitynode.opacity✅
mix-blend-modeLayerBlendMode::Blend(BlendMode)✅All 16 modes

Border​

CSS PropertyGrida IR FieldStatusNotes
border-widthStrokeWidth (Uniform or Rectangular)✅Per-side widths
border-colorPaints (strokes)✅Single color for all sides
border-style (solid/dashed/dotted)StrokeStyle.stroke_dash_array✅
border-radiusRectangularCornerRadius✅Per-corner, elliptical
Per-side border colors--⚠️Only first visible side color used
border-image--❌No gradient/image stroke

Shadow & Effects​

CSS PropertyGrida IR FieldStatusNotes
box-shadowFilterShadowEffect::DropShadow✅dx/dy/blur/spread/color
box-shadow: insetFilterShadowEffect::InnerShadow✅
text-shadowFilterShadowEffect::DropShadow✅On TextSpan nodes; no spread
filter: blur()FeLayerBlur✅
filter: drop-shadow()FilterShadowEffect::DropShadow✅
backdrop-filter: blur()FeBackdropBlur🔧Code exists; Stylo servo mode blocks
filter: brightness()--❌No IR for non-blur filters
filter: contrast()--❌
filter: grayscale()--❌
filter: sepia()--❌
filter: hue-rotate()--❌
filter: invert()--❌
filter: saturate()--❌

Text​

CSS PropertyGrida IR FieldStatusNotes
font-sizeTextStyleRec.font_size✅
font-weightTextStyleRec.font_weight✅
font-familyTextStyleRec.font_family✅
font-style: italicTextStyleRec.font_style_italic✅
colorTextSpanNodeRec.fills (Solid)✅Inherited
text-alignTextAlign✅left/right/center/justify
line-heightTextLineHeight (Factor or Fixed)✅
letter-spacingTextLetterSpacing::Fixed✅
word-spacingTextWordSpacing::Fixed✅
text-transformTextTransform✅uppercase/lowercase/capitalize
text-decoration-lineTextDecorationRec.text_decoration_line✅underline/overline/line-through
text-decoration-colorTextDecorationRec.text_decoration_color✅
text-decoration-styleTextDecorationRec.text_decoration_style✅solid/double/dotted/dashed/wavy
text-decoration-thicknessTextDecorationRec.text_decoration_thickness🔧IR field exists; Stylo servo mode doesn't expose it (gecko-only)
text-decoration-skip-inkTextDecorationRec.text_decoration_skip_ink🔧IR field exists; Stylo servo mode doesn't expose it (gecko-only)
white-space--❌Not enforced
text-overflow--❌No IR field
vertical-align--❌No baseline offset
text-indent--❌No IR field
font-variant--❌Not mapped

Transform​

CSS PropertyGrida IR FieldStatusNotes
transform (2D)AffineTransform🔧IR exists on every node; CSS import not wired
transform (3D)--❌IR is 2D only
transform-origin--❌No pivot point in IR

Overflow & Clip​

CSS PropertyGrida IR FieldStatusNotes
overflowContainerNodeRec.clip✅Single bool for both axes
overflow-x / overflow-yclip⚠️Merged to single bool
clip-path--❌No arbitrary clip shape

Visibility​

CSS PropertyGrida IR FieldStatusNotes
visibility: hidden--❌Needs dedicated field; NOT opacity:0 (see below)
visibility: collapse--❌Same

Design note: visibility: hidden keeps layout space, suppresses paint, blocks pointer events, and is overridable by children (visibility: visible). None of these semantics match opacity: 0. The IR needs a per-node visible: bool field. Chromium implements this as a paint-skip flag, not a compositing trick.

Interaction (out of scope)​

CSS PropertyStatusNotes
cursor🚫Runtime-only
pointer-events🚫Runtime-only
user-select🚫Runtime-only
transition / animation🚫Static format
@keyframes🚫Static format

IR Gaps​

Properties blocked by missing schema fields, grouped by the change that would unblock them.

1. LayoutChildStyle expansion​

Unblocks: flex-shrink, flex-basis, align-self, margin, order, top/right/bottom/left

Current LayoutChildStyle only has layout_grow: f32 and layout_positioning: LayoutPositioning. Adding shrink, basis, self-alignment, and margins would complete the flex-child model.

2. Visibility field​

Unblocks: visibility: hidden, visibility: collapse

Needs a per-node visible: bool (or enum). Must be inherited and child-overridable. Distinct from opacity and active.

3. Grid layout​

Unblocks: display: grid, grid-template-columns, grid-template-rows, grid-area, place-items

Requires a new LayoutMode::Grid and track definition types.

4. Non-blur filter functions​

Unblocks: brightness(), contrast(), grayscale(), sepia(), hue-rotate(), invert(), saturate()

Could be modeled as a color matrix or individual filter effect variants.

5. Transform origin​

Unblocks: transform-origin

Currently transforms are applied around (0, 0). A pivot point field on AffineTransform or the node would enable center/corner-based transforms.

6. Flex direction reverse​

Unblocks: flex-direction: row-reverse / column-reverse, flex-wrap: wrap-reverse

Axis enum only has Horizontal/Vertical. Needs reverse variants or a separate bool.


Known Limitations​

Margin collapse is not supported​

CSS spec: CSS Box Model Level 3 §5 — Collapsing Margins, originally CSS2 §8.3.1.

In CSS block formatting context (normal flow), adjacent vertical margins do not stack — they collapse into max(margin_a, margin_b). This applies to all block-level elements (not just text), in three cases:

  1. Sibling collapse — bottom margin of element A meets top margin of element B
  2. Parent-child collapse — child's margin leaks through a parent with no padding/border
  3. Empty element collapse — an element's own top and bottom margins merge

Grida converts display: block to LayoutMode::Flex (column), and flex containers never collapse margins (CSS Flexbox §4.4). This is an inherent limitation — Taffy's flex layout engine does not implement block formatting context, and no design tool (Figma, Framer, Sketch) does either.

Impact: Imported HTML that relies on margin collapse will have more vertical spacing than the original. Two sibling elements with margin-bottom: 20px and margin-top: 30px produce a 50px gap instead of the expected 30px.

Workaround: None at the engine level. Content authors can use gap on flex containers instead of sibling margins, which is the modern CSS best practice and avoids collapse entirely.

Margin auto is not supported​

CSS margin: auto in flex containers absorbs free space per-child, enabling centering and push-alignment patterns that cannot be expressed with padding or container-level alignment alone. This requires either a first-class margin: auto field on LayoutChildStyle, or SpacerNode siblings (Flutter's Spacer widget pattern). Neither is implemented yet.

Negative margins are not supported​

CSS allows negative margins to pull elements closer or create overlapping layouts. Padding cannot be negative, so the wrapper+padding tree surgery cannot represent this. Negative margins are silently dropped during import.


Tree Surgery Reference​

CSS properties that lack direct IR representation are converted via structural tree transforms during HTML import (crates/grida/src/import/html/mod.rs).

Margin → wrapper + padding​

Fixed positive margins are handled by wrapping the element in a transparent container whose padding equals the margin values. The wrapper inherits the element's layout_child role (flex-grow, positioning) so it occupies the correct slot in the parent's flex layout.

For containers without visual properties (no fills, no strokes), the margin is merged directly into the container's own padding to avoid an unnecessary wrapper node.

CSS margin patternTree surgeryAccurate?Notes
margin: 20px (fixed uniform)Wrapper { padding: 20px } → childYesExact in flex (no collapse)
margin: 10px 20px 30px 40pxWrapper with matching asymmetric paddingYesExact in flex
margin: 0No-opYes
margin: 0 auto (center)Not supported—Requires SpacerNode or first-class margin: auto
margin-left: auto (push)Not supported—Same
margin-top: -20px (negative)DroppedNoPadding cannot be negative
margin: 5% (percentage)DroppedNoWould need computed value at import time
Sibling collapse (margin-bottom + margin-top)Summed, not collapsedNoFlex does not collapse margins; inherent limitation

Future: SpacerNode for auto margins​

Auto margins can be structurally represented by inserting invisible SpacerNode(flex_grow: 1) siblings, equivalent to Flutter's Spacer widget:

CSS: [A] [B margin-left:auto] [C]
IR: [A] [SpacerNode(1)] [B] [C]

CSS: [A margin:0 auto]
IR: [SpacerNode(1)] [A] [SpacerNode(1)]

Future: first-class margin on LayoutChildStyle​

If the wrapper approach proves insufficient (tree bloat, round-trip fidelity), margin can be added as Option<MarginEdgeInsets> on LayoutChildStyle with per-edge Fixed(f32) / Auto variants, wired directly to Taffy's Style.margin: Rect<LengthPercentageAuto>. See format/grida.fbs LayoutChildStyle table for the schema extension point.

Fixture​

Visual test fixture for all margin behaviors: fixtures/test-html/L0/box-margin.html