본문으로 건너뛰기

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-canvas/src/html/), SVG import (via usvg, crates/grida-canvas/src/svg/).


Box Model & Sizing

CSS PropertyGrida IR FieldStatusNotes
widthLayoutDimensionStyle.layout_target_widthpx only; % not resolved
heightLayoutDimensionStyle.layout_target_heightpx 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_paddingEdgeInsets (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_alignmentstart/center/end/stretch
justify-contentlayout_main_axis_alignmentstart/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_gapDirection-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 fillshex/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-radiusRectangularCornerRadiusPer-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::DropShadowdx/dy/blur/spread/color
box-shadow: insetFilterShadowEffect::InnerShadow
text-shadowFilterShadowEffect::DropShadowOn 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-alignTextAlignleft/right/center/justify
line-heightTextLineHeight (Factor or Fixed)
letter-spacingTextLetterSpacing::Fixed
word-spacingTextWordSpacing::Fixed
text-transformTextTransformuppercase/lowercase/capitalize
text-decoration-lineTextDecorationRec.text_decoration_lineunderline/overline/line-through
text-decoration-colorTextDecorationRec.text_decoration_color
text-decoration-styleTextDecorationRec.text_decoration_stylesolid/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.clipSingle 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-canvas/src/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 supportedRequires SpacerNode or first-class margin: auto
margin-left: auto (push)Not supportedSame
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