Current as of:
66ce7bc(2026-03-02). Read OVERVIEW.md first.
Notation used throughout:
?= optional,!= requireddef=default value→return typepx|tok= number (pixels) or spacing token string- DO: / DONT: = best practice / anti-pattern
These appear on most element types. When a section says "+ CP", all of these apply:
id: string def=auto("sk-N")— unique identifierx: PositionValue def=0— X position (number, percentage string"50%","safe:25%", or RelMarker)y: PositionValue def=0— Y position (same types as x)w: SizeValue?— width in px, percentage string,"fill"in panels,matchWidthOf(refId), ormatchMaxWidth(group). Auto-measured if omitted.h: SizeValue?— height in px, percentage string,matchHeightOf(refId), ormatchMaxHeight(group). Auto-measured if omitted.maxW: number?— max width constraintmaxH: number?— max height constraintanchor: AnchorPoint def="tl"— which point sits at (x,y); see Anchor Systemlayer: "bg"|"content"|"overlay" def="content"— render layervAlign: "top"|"center"|"bottom" def="top"— vertical alignment of content within boxopacity: number def=1— 0–1rotate: number?— degreesflipH: boolean?— horizontal flip (scaleX(-1))flipV: boolean?— vertical flip (scaleY(-1))overflow: "visible"|"warn"|"clip"|"error" def="visible"— overflow policyshadow: string?— preset name (sm|md|lg|xl|glow) or CSSbox-shadowvaluez: number?— explicit z-order within same layer (higher = front)allowOverlap: boolean def=false— skip linter overlap checksstyle: object def={}— CSS pass-through (applied to container div)className: string def=""— CSS class(es)
Overflow policies: visible = render fully; warn = emit warning on overflow; clip = CSS overflow:hidden; error = error on overflow.
Call before using any SlideKit function.
await init({
slide: { w: 1920, h: 1080 },
safeZone: { left: 120, right: 120, top: 90, bottom: 90 },
minFontSize: 24,
fonts: [{ family: 'Inter', weights: [400, 600, 700], source: 'google' }],
});Config fields (all optional):
slide.w: number def=1920,slide.h: number def=1080safeZone.{left,right}: number def=120,safeZone.{top,bottom}: number def=90strict: boolean def=false— safe zone/bounds violations become errorsminFontSize: number def=24— below this triggers readability warningsspacing: Record<string,number>— custom tokens merge with defaults (see Spacing)fonts: FontDef[]—{ family, weights?, source? }— preloaded; Google Fonts via<link>, 5s timeout per weightminVersion: string?— semver compatibility check (throws on mismatch)debug.sourceHint: string?— path to slide source file, shown in inspector
Version compatibility: 0.x and 1.x are same group. From 2.0+, major must match. Minor mismatch → console.warn. Breaking mismatch → throws with pointer to docs/MIGRATION.md.
checkVersionCompatibility(minVersion, currentVersion) → { ok, warning?, error? } — low-level check used by init.
Throws if safe zone leaves zero/negative usable area.
Requires browser environment (DOM) for measurement, font loading, Reveal.js rendering.
Single element primitive. Creates a positioned HTML element.
el('<p style="font:700 52px/1.1 Inter;color:#fff">Hello</p>', { id: 'title', x: 960, y: 400, w: 800, anchor: 'tc' });Returns { id, type: "el", content: html, props, _layoutFlags }. Accepts all CP.
Replaces v1 primitives:
- Text:
el('<p style="font:700 52px Inter">Title</p>', { x, y, w }) - Image:
el('<img src="photo.jpg" style="width:100%;height:100%;object-fit:cover">', { x, y, w, h }) - Rect:
el('', { x, y, w, h, style: { background: '#1a1a3e', borderRadius: '12px' } }) - Rule/divider:
el('', { x, y, w: 80, h: 3, style: { background: '#7c5cbf' } })
DONT: Put CSS props like fontSize at top level — put inside style: {}. Auto-promoted with warning; linter flags misplaced-css-prop.
DONT: Omit w on long text — measure() computes unconstrained single-line width. Always set w for paragraph text.
DO: Omit h on text elements — let measure() compute actual height. Only set h for fixed-size containers (panels, image frames).
DO: Use element prop overflow not CSS style: { overflow } — CSS overflow is blocked by filterStyle().
DO: layer: 'bg' for full-bleed backgrounds, layer: 'overlay' for decorative edges. Both exempt from safe-zone rules.
Container with shared coordinate origin. Moving group moves all children.
Returns { id, type: "group", children, props, _layoutFlags }. Accepts all CP plus:
scale: number def=1clip: boolean def=false— clip children to group boundsbounds: "hug"?— auto-compute w/h from children's bounding box (skips unresolved_relmarkers and compound groups)
DO: bounds: 'hug' for containers whose size derives from content. Linter suppresses child-overflow for hug groups.
DONT: Create groups without dimensions or bounds: 'hug' when used as layout containers — defaults to 0×0.
DO: Children use group-relative coordinates. Group moves = children move.
Stacks arrange children in sequence, resolving to absolute coordinates during layout. Not CSS flexbox.
DO: Prefer vstack/hstack over manual below()/rightOf() chains.
DONT: Use relative positioning (below(), rightOf()) on stack children — ignored with ignored_rel_on_stack_child warning.
Stacks items vertically. Returns { id, type: "vstack", children, props, _layoutFlags }. Accepts all CP plus:
gap: px|tok def=0— vertical gapalign: "left"|"center"|"right"|"stretch" def="left"— cross-axis (horizontal) alignmentvAlign: "top"|"center"|"bottom" def="top"— main-axis vertical alignment (requires explicith)
If stack has w and child lacks w, child inherits stack width.
Stacks items horizontally. Returns { id, type: "hstack", children, props, _layoutFlags }. Accepts all CP plus:
gap: px|tok def=0— horizontal gapalign: "top"|"middle"|"bottom"|"stretch" def="top"— cross-axis (vertical) alignmenthAlign: "left"|"center"|"right" def="left"— main-axis horizontal alignment (requires explicitw)
vAlign/hAlign position the entire content block within the stack's explicit dimension. Requires explicit h (vstack) or w (hstack); otherwise no-op. align (cross-axis) and vAlign/hAlign (main-axis) are orthogonal — can combine.
- vstack: all children get same width (widest child or stack's
w). Auto-heightel()re-measured at new width. - hstack: all children get same height (tallest child or stack's
h).
Explicit size override emits stretch_override warning.
DO: hstack({ align: 'stretch' }) for equal-height card layouts.
Grid via nested vstack/hstack. Each row is hstack with align: 'stretch'.
Options: id?, cols: number def=2, gap: px|tok def=0, x?, y?, w?, anchor?, layer?, style?
Row IDs: {id}-row-0, {id}-row-1, etc.
DONT: Linter can't resolve child positions inside hstack()/cardGrid(). For lint-friendly multi-column, use splitRect() or manual arithmetic.
Visual container with background, padding, vertically stacked children. Internally: group containing bg el('') + vstack of children.
Additional props beyond CP:
padding: px|tok def=24gap: px|tok def=16— vertical gap between childrenfill: string?— background color/gradient (→ CSSbackground)radius: number|string?— border radiusborder: string?— CSS border shorthandvAlign: "top"|"center"|"bottom" def="top"— main-axis alignment (requires explicith)
Panel content measured at full render width, not panel width — text wrapping may differ in narrow panels. Set explicit w on text children in narrow panels. Children with w: "fill" → panelWidth - 2*padding.
Children use panel-relative coordinates (after padding).
When panel has explicit h and content exceeds it → panel_overflow warning. Fix: remove h or increase it.
DONT: w: 'fill' in nested panels — inner fill resolves before outer sets width.
DO: When panel children cause false-positive overlaps, flatten to single el() with inline HTML.
Connector line between two elements. SVG with optional arrowheads/labels. Endpoints resolved during layout Phase 4.
Returns { id, type: "connector", props, _layoutFlags }.
Props:
id?, type: "straight"|"curved"|"elbow"|"orthogonal" def="straight"arrow: "none"|"start"|"end"|"both" def="end"color: string def="#ffffff",thickness: number def=2dash: string|null def=null— SVGstroke-dasharray(e.g.,"5 5"= dashed,"10 4 2 4"= dash-dot,"2 4"= dotted)fromAnchor: AnchorPoint def="cr",toAnchor: AnchorPoint def="cl"label: string|null def=null,labelStyle: { size?, color?, font?, weight? } def={}cornerRadius: number def=0— rounding of corners on elbow/orthogonal connectors (0 = sharp corners)obstacleMargin: number def=200— search margin for obstacle avoidance on orthogonal connectorslayer?, opacity?
Connector types: straight = direct line; curved = cubic bézier 40% offset along dominant axis; elbow = orthogonal path via routeConnector(); orthogonal = orthogonal path with obstacle avoidance (like elbow but routes around other elements).
DO: For visible curves, use corner anchors (tr→tl) not center-edge — center-edge on horizontally-aligned elements produces collinear control points (curve looks straight).
DONT: dash expects SVG format "5 5", not CSS "6,4" — incorrect format silently produces solid line.
tl --- tc --- tr
| |
cl cc cr
| |
bl --- bc --- br
First char: row (t/c/b). Second char: column (l/c/r).
resolveAnchor(x, y, w, h, anchor) → { left, top } — converts anchor-relative to CSS top-left. Throws on invalid anchor.
getAnchorPoint(bounds: Rect, anchor) → { x, y, dx, dy } — pixel coordinates + outward direction vector.
DO: anchor: 'tc' with x: 960 to center on slide. anchor: 'tl' with x: safe.x to left-align at safe zone.
Return RelMarker objects resolved during layout via dependency graph (topological sort). Circular deps → error. Only valid on x/y — using on w/h → error.
Position helpers (all: (refId, opts?: { gap?: px|tok }) → RelMarker):
below(ref, {gap?})→ Y = ref bottom + gapabove(ref, {gap?})→ Y = ref top − element height − gaprightOf(ref, {gap?})→ X = ref right + gapleftOf(ref, {gap?})→ X = ref left − element width − gap
Alignment helpers (all: (refId) → RelMarker):
centerVWith(ref)→ Y centers vertically with refcenterHWith(ref)→ X centers horizontally with refalignTopWith(ref)→ Y = ref topalignBottomWith(ref)→ Y = ref bottom − element heightalignLeftWith(ref)→ X = ref leftalignRightWith(ref)→ X = ref right − element width
Axis rule: alignTop/alignBottom/centerV → Y axis. alignLeft/alignRight/centerH → X axis.
DONT: Cross axes — alignTopWith() (Y) used as X coordinate silently produces wrong positions.
DO: centerVWith(ref) to vertically center a label next to a taller element.
Slide-level centering (no ref needed):
centerHOnSlide()→ X centers horizontally on the slide (returns{ _rel: 'centerHSlide' })centerVOnSlide()→ Y centers vertically on the slide (returns{ _rel: 'centerVSlide' })
el('Title', { x: centerHOnSlide(), y: centerVOnSlide(), w: 800, anchor: 'tc' });Dimension constraints (valid on w/h, not x/y):
matchWidthOf(ref)→{ _rel: 'matchWidth', ref }— element width = ref's widthmatchHeightOf(ref)→{ _rel: 'matchHeight', ref }— element height = ref's heightmatchMaxWidth(group)→{ _rel: 'matchMaxWidth', group }— element width = widest in groupmatchMaxHeight(group)→{ _rel: 'matchMaxHeight', group }— element height = tallest in group
el('Body', { y: below('header', 'md'), w: matchWidthOf('header') });
// Group-max: all three cards match the tallest one's height
el('Card A', { id: 'a', x: 0, y: 0, w: 300, h: matchMaxHeight('row') });
el('Card B', { id: 'b', x: 340, y: 0, w: 300, h: matchMaxHeight('row') });DO: Use matchWidthOf/matchHeightOf to constrain a single element to a reference. Use matchMaxWidth/matchMaxHeight to equalize sizes across a named group. Use transform matchWidth/matchHeight for batch operations on multiple elements.
Positions element in the gap between two references on a given axis.
refA: string|number— element ID or raw px coordinaterefB: string|number— element ID or raw px coordinate (at least one must be an element ID)axis: 'x'|'y'— required. Which axis to position on.bias: number def=0.5— 0.0 = flush to refA edge, 1.0 = flush to refB edge, 0.5 = centered
When axis: 'x': positions in the horizontal gap between refA's right edge and refB's left edge.
When axis: 'y': positions in the vertical gap between refA's bottom edge and refB's top edge.
// Center breakout box horizontally between two nodes
x: between('node3', 'node5', { axis: 'x' }),
// Place label between header and content, biased toward top
y: between('header', 'content', { axis: 'y', bias: 0.35 }), // explicit bias overrides default 0.5
// Between an element and a raw coordinate
y: between('lastCard', 990, { axis: 'y' }),DONT: Falls back if element doesn't fit. Check for between_no_fit warnings.
DONT: Assign to the wrong prop (e.g., y: between(..., { axis: 'x' })). Emits between_axis_mismatch warning.
Deprecated: Use
between(refA, refB, { axis: 'y' })instead.
Positions element vertically between two references.
topRef: string— ID above (uses bottom edge)bottomYOrRef: string|number— ID below (uses top edge) or raw Y pxopts.bias: number def=0.5— 0.0 = flush top, 1.0 = flush bottom, 0.5 = centered
DONT: Falls back silently if element doesn't fit. Ensure gap ≥ element height. Check for between_no_fit warnings.
Centers element within a rectangle (both axes). el('<p>Centered</p>', { ...centerIn(safe), w: 400 })
PowerPoint-style operations applied during layout Phase 3 via transforms array on slide definition. All return TransformMarker: { _transform, _transformId, ids, options }.
DONT: Transforms only move listed elements. If distributing containers but not labels, labels stay behind. Group related elements first, then transform groups.
alignLeft / alignRight / alignTop / alignBottom / alignCenterH / alignCenterV
Defaults for to: min left edge / max right edge / min top / max bottom / avg H center / avg V center.
distributeH(ids, opts?) → TransformMarker — equal horizontal gaps.
Options: startX?: number, endX?: number, mode?: "equal-gap"|"equal-center" def="equal-gap"
distributeV(ids, opts?) — same but vertical; uses startY, endY.
DONT: Fewer than 2 elements silently does nothing.
matchWidth — all widths → max width. matchHeight — all heights → max height. matchSize — both.
Scales + centers elements to fit within rect, preserving aspect ratio.
style prop passes CSS to rendered container. SlideKit owns layout; you own styling (colors, gradients, shadows, fonts, borders, animations).
DO: textAlign should match positioning direction. With leftOf() → style: { textAlign: 'right' }.
color→color, font→fontFamily, size→fontSize+"px", weight→fontWeight, fill→background, radius→borderRadius, border→border, align→textAlign, shadow→boxShadow. If both convenience and style set same CSS prop, style wins.
These CSS props are blocked from style (conflict with positioning system) and emit warnings:
- Layout:
position, top, left, right, bottom, inset* - Sizing:
width, height, min/maxWidth, min/maxHeight, *Size(block/inline variants) - Display:
display - Overflow:
overflow, overflowX/Y, overflow*(use element propoverflowinstead) - Margin:
margin*(all variants) - Transform:
transform, translate, rotate, scale - Containment:
contain, contentVisibility - Vendor-prefixed variants also blocked.
filterStyle(style?, elementType?) → { filtered, warnings } — removes blocked props.
detectMisplacedCssProps(props) → { cssProps, warnings } — detects CSS props at element top-level, auto-promotes to style. CSS_LIKE_PROPS ReadonlySet includes: textAlign, fontSize, fontFamily, fontWeight, background, backgroundColor, border, borderRadius, boxShadow, padding, lineHeight, letterSpacing, whiteSpace, textDecoration, textTransform, cursor, visibility, etc.
DONT: Rely on auto-promotion — always write CSS inside style: {}.
SHADOWS: sm = "0 1px 3px rgba(0,0,0,0.2)", md = "0 4px 12px rgba(0,0,0,0.3)", lg = "0 8px 32px rgba(0,0,0,0.4)", xl = "0 16px 48px rgba(0,0,0,0.5)", glow = "0 0 40px rgba(124,92,191,0.3)".
resolveShadow(value) → string — resolves preset name to CSS; non-preset returned unchanged.
getShadowPresets() → Record<string,string> — shallow copy of presets map.
DONT: glow shadows only visible on dark backgrounds.
Injected inside every el() container. Neutralizes inherited styles from host frameworks (Reveal.js). Key defaults: text-align: left, line-height: 1.2, font-weight: 400, all margins → 0, headings inherit font, media max-width/max-height: none. User inline styles always win.
DO: For text on semi-transparent backgrounds, use opacity ≥ 0.25. Opacity 0.15 fails contrast checks (linter flags < 4.5:1 ratio).
className prop references classes from loaded stylesheets. Theme CSS must contain only visual styling (colors, gradients, borders, shadows, typography, animations). Never layout props (position, display, width, height, margin, padding, flex, grid) — they conflict with SlideKit positioning.
xs=8, sm=16, md=24, lg=32, xl=48, section=80 (px).
Usable anywhere gap/padding accepted. Customizable via init({ spacing }) — merges with defaults.
DO: Use tokens instead of magic numbers. Linter flags gaps < 8px (gap-too-small).
DONT: Gaps < xs (8px) between text — fails readability checks.
Numbers pass through. Strings looked up in scale → number. undefined/null pass through. Unknown token → throws with list of available tokens.
Public API. getSpacing('md') → 24. Numbers pass through. Throws on unknown token.
Computes orthogonal polyline for elbow connectors.
Options:
from: AnchorPointResult!— source with direction vectorto: AnchorPointResult!— target with direction vectorobstacles: Rect[] def=[]stubLength: number def=30— mandatory orthogonal stub lengthclearance: number def=15— minimum obstacle clearance
Renders slide definitions into DOM as absolutely-positioned elements inside Reveal.js <section> elements.
Options: container: HTMLElement def=document.querySelector('.reveal .slides')
id: string— slide ID (→section.id)background: string— color, gradient, or image pathelements: SlideElement[]transforms: TransformMarker[]notes: string— speaker notes (→<aside class="notes">)
Background handling (applySlideBackground): solid colors → data-background-color; gradients → data-background-gradient; images → data-background-image.
window.sk = {
layouts: [/* LayoutResult per slide */],
slides: [{ id, layout }],
_config: {/* current config */},
lint(slideId) → LintFinding[],
lintDeck() → LintFinding[],
};DO: After code changes, hard-reload (Ctrl+Shift+R or ?v=timestamp) and re-lint. Browsers cache aggressively.
DO: Take screenshots after programmatic checks for visual verification.
Built into the main bundle (no separate debug bundle). Toggle with keyboard shortcut:
enableKeyboardToggle(); // Ctrl+. to toggle inspector overlayFeatures: bounding-box overlays, element list with layer grouping and hide checkboxes, visibility toggles for overlay features, live-preview HTML editing, snap-to-10px grid during drag/resize (Shift to disable snap).
After rendering, render() checks every el for overflow (scrollHeight > clientHeight + 1px). Emits dom_overflow_y / dom_overflow_x warnings.
computeZOrder(elements) → Map<string,number>— sorts by layer (bg<content<overlay), thenz(def 0), then declaration order.applyStyleToDOM(domEl, styleObj)— applies CSS object; handles--*custom props viasetProperty().renderElementFromScene(element, zIndex, sceneElements, offsetX?, offsetY?) → HTMLElement— renders single element from scene graph.
Core layout engine. Resolves positions, measures HTML, detects collisions, validates bounds.
Options: collisionThreshold: number def=0 — minimum overlap area (px²) to report.
Returns LayoutResult: { elements, rootIds, transforms, warnings, errors, collisions }
Map of id → SceneElement:
id, type, authored(original spec),resolved: Rect(final absolute position),localResolved: Rect(parent-relative)parentId: string|null,children: string[],provenance: object,_internal: boolean(panel internals)
rootIds — ordered array of top-level IDs (no parent).
Provenance sources: authored (user props), measured (HTML measurement), constraint (RelMarker), stack (parent stack), transform (layout transform), default.
{ type, elementId, message }:
outside_safe_zone,outside_slide— bounds violations (bg layer exempt)content_clustered,content_no_breathing_room— spacing issuesstretch_override— child size overridden by stretchpanel_overflow— panel content exceeds authored heightdom_overflow_y/x— post-render DOM overflowtransform_unknown_element— transform references non-existent ID
dependency_cycle— circular relative positioningunknown_ref— RelMarker references non-existent IDinvalid_rel_on_dimension— RelMarker onw/h(only valid onx/y)
Strict mode (init({ strict: true })): bounds violations become errors.
{ elementA, elementB, overlapArea, overlapRect: Rect } — per-layer (different layers don't collide).
- Phase 1 — Intrinsic Sizes: measure HTML, resolve stack sizes, percentages, panel auto-height,
bounds: 'hug' - Phase 2 — Positions: dependency graph from RelMarkers → topological sort → absolute coords, anchor resolution, stack child positions
- Phase 3 — Transforms: alignment/distribution/sizing in declaration order
- Phase 4 — Finalize: scene model, connector endpoints, collision detection, bounds validation, panel overflow
Computes effective w/h. For el() without explicit h, measures HTML.
Off-screen DOM measurement of HTML content.
Props: w?: number (constraining width), className?: string, style?: object (filtered through filterStyle).
Without w: natural width/height. With w: constrained width, measured height. Images waited on (3s timeout). Cached by (html, w, style, className).
Clears measurement cache. Call if fonts change.
slideData: { id, layout: { elements, warnings, errors, collisions } }. Optional slideElement: HTMLElement.
skData: { slides: LintSlideData[] }. Optional sections: NodeListOf<HTMLElement>.
{ rule, severity: "error"|"warning"|"info", elementId, message, detail, bounds?, parentBounds?, suggestion, slideId? }
duplicate-id(error): Two+ elements share sameidon same slide. Runs first — duplicates cause cascading issues.
window.sk.lint(slideId) → LintFinding[] — per-slide (fast, use during dev).
window.sk.lintDeck() → LintFinding[] — all slides (cross-slide checks: title-position-drift, style-drift).
DO: Filter findings: findings.filter(f => f.severity !== 'info'). Focus on text-overflow, canvas-overflow, non-ancestor-overlap.
DO: allowOverlap: true on intentionally overlapping elements.
DONT: Suppress overflow with overflow: 'hidden' to silence lint — fix dimensions instead.
Returns safe zone rectangle. Default: { x: 120, y: 90, w: 1680, h: 900 }. Throws if init() not called.
DO: Use safeRect() instead of hardcoded numbers. Content stays within x: 120–1800, y: 90–990.
Splits rectangle for two-column layouts.
rect: Rect!(typicallysafeRect())options.ratio: number def=0.5— fraction of usable width for left (0–1)options.gap: px|tok def=0
const { left, right } = splitRect(safeRect(), { ratio: 0.55, gap: 40 });DO: Use splitRect() over manual column math — lint-friendly explicit coordinates.
DONT: Use left.h/right.h as content height — it's max available. Omit h on content containers; use h only on full-bleed elements (images, bg rects).
Deep copy of current config, or null if init() not called.
Resets all state (config, safe rect, ID counters, fonts, cache, DOM). Testing only.
Resets auto-ID counter. Called automatically at start of layout() and render().
Column grid for consistent positioning.
cols: number def=12,gutter: number def=30,margin: { left?, right? } def=safe zone margins
Returns: cols, gutter, colWidth, marginLeft, marginRight, totalWidth, col(n) (X of column n, 1-based), spanW(from, to) (width spanning columns, inclusive, 1-based). Throws if colWidth ≤ 0.
Rounds to nearest grid increment. Returns unchanged if gridSize ≤ 0.
"N%" → % of slide dimension. "safe:N%" → % of safe zone. Non-strings pass through. Resolved automatically during layout Phase 1.
Duplicates element in grid pattern.
count: number def=1,cols: number def=countgapX: px|tok def=0,gapY: px|tok def=0startX: number def=0,startY: number def=0
Returns group of deep-cloned copies with IDs {baseId}-{i+1}.
Axis-aligned bounding box of rotated rectangle.
Rect = { x, y, w, h } · Point = { x, y } · AnchorPointResult = { x, y, dx, dy }
SlideElement = ElElement | GroupElement | VStackElement | HStackElement | ConnectorElement
ElElement = { type: "el", id, content, props, _layoutFlags }GroupElement = { type: "group", id, children, props, _layoutFlags }VStackElement = { type: "vstack", id, children, props, _layoutFlags }HStackElement = { type: "hstack", id, children, props, _layoutFlags }ConnectorElement = { type: "connector", id, props, _layoutFlags }PanelElement= Group with_compound: "panel",_panelConfig
AnchorPoint:tl|tc|tr|cl|cc|cr|bl|bc|brLayerName:bg|content|overlayVAlign:top|center|bottomOverflowPolicy:visible|warn|clip|errorHAlign:left|center|right|stretchHStackAlign:top|middle|bottom|stretchConnectorType:straight|curved|elbow|orthogonalArrowType:none|end|start|bothElementType:el|group|vstack|hstack|connectorCompoundType:panelBoundsMode:hug|undefinedProvenanceSource:authored|measured|stack|constraint|transform|defaultShadowPreset:sm|md|lg|xl|glow
PositionValue = number | string | RelMarkerSizeValue = number | string | RelMarkerRelMarker = { _rel: RelMarkerKind, ref?, ref2?, gap?, bias?, axis?, rect? }RelMarkerKind: positional (below|above|rightOf|leftOf|alignTop|alignBottom|alignLeft|alignRight|centerV|centerH|centerHSlide|centerVSlide|centerIn|between), dimensional (matchWidth|matchHeight)TransformMarker = { _transform, _transformId, ids, options }
SlideKitConfig = { slide, safeZone, strict, minFontSize, fonts, spacing }FontDef = { family, weights?, source? }SpacingScale = { xs?, sm?, md?, lg?, xl?, section?, [key]: number }SlideDefinition = { id?, background?, elements, transforms?, notes? }VersionCheckResult = { ok, warning?, error? }
LayoutResult = { elements, rootIds, transforms, warnings, errors, collisions }SceneElement= full resolved element in scene graphCollision = { elementA, elementB, overlapRect, overlapArea }StyleFilterResult = { filtered, warnings }
InputProps = ElementProps & { id? }— input for all element factoriesElementProps= all CP +gap, align, vAlign, hAlign, bounds, scale, clipCardGridOptions = InputProps & { cols? }ConnectorInputProps = { id?, type?, arrow?, color?, thickness?, dash?, fromAnchor?, toAnchor?, label?, labelStyle?, cornerRadius?, obstacleMargin?, layer?, opacity?, style?, className? }PanelInputProps = InputProps & { padding?, fill?, radius?, border?, vAlign? }
DONT: Specify h on text elements — overrides DOM measurement, leading cause of text-overflow bugs.
DONT: Calculate height mathematically (fontSize * lineHeight) — browser metrics make this inaccurate.
DONT: Specify h just for positioning math — use below() chaining instead.
DONT: Specify h to vertically center — use vAlign: 'center' in a vstack/panel instead.
DONT: Use h: left.h from splitRect() on content containers — that's full column height.
DO: Omit h on all text elements — SlideKit auto-measures from DOM.
DO: Always specify w on text — constrains wrapping so height measurement is accurate.
DO: Only specify h for fixed-size containers (panels, terminal blocks, image frames).
These were discovered by AI agents building real presentations:
DONT: Create el() without explicit w — collapses to 0px, invisible. Always set w or use w: 'fill' in a container.
DONT: Create groups without w/h or bounds: 'hug' when used as layout containers — defaults to 0×0, children invisible.
DONT: Let below() chains accumulate beyond safe zone. Total budget: 990 − 90 = 900px for all elements + gaps. Verify last element bottom ≤ 990.
DONT: Assume w: 'fill' equals parent width — padding and siblings reduce it. Check window.sk.layouts[N].elements['id'].resolved.w.
DONT: Use unencoded special characters in SVG data URIs — use encodeURIComponent().
DONT: Use parentId to suppress overlap warnings unless the parent-child relationship is actually correct.
DONT: Use tight gaps (< 30px) after large visuals — captions look cramped. Use max(30px, 1.5× caption font size).
DO: For 3+ column layouts, compute positions explicitly instead of hstack() — eliminates 10-16 false-positive overlaps per slide: cardW = (safe.w - gap*(cols-1)) / cols; x = safe.x + col*(cardW+gap).
DO: When panel([child1, child2]) causes false overlaps, flatten to single el() with inline HTML.
DO: Fill the safe zone width with cards — narrow cards waste space and trigger content-clustering.
DO: Render images first, read img.naturalWidth/naturalHeight, compute exact container so object-fit: contain is a no-op.
DO: CSS framework resets go in _baselineCSS() (style.ts), not inline on every element.
Large centered text (anchor: 'tc', x: 960, font 72–96px), subtitle below with generous gap, decorative elements on layer: 'bg'. Sparse by design — content-clustering warnings expected.
const { left, right } = splitRect(safeRect(), { ratio: 0.5, gap: 40 });
el(content, { x: left.x, y: left.y, w: left.w });
el(diagram, { x: right.x, y: right.y, w: right.w });const cols = 3, gap = 20;
const cardW = (safe.w - gap * (cols - 1)) / cols;
cards.forEach((card, i) => {
const col = i % cols, row = Math.floor(i / cols);
panel(card.children, { x: safe.x + col * (cardW + gap), y: topY + row * (cardH + gap), w: cardW });
});text-overflow: all text visible?scrollHeight ≤ clientHeightcanvas-overflow: all elements within 1920×1080?- Safe zone: content-layer within x:120–1800, y:90–990?
- No unintended sibling overlaps
- Gaps ≥ 8px between text elements
- Contrast ≥ 4.5:1 for body text
textAlignmatches direction (leftOf→right,rightOf→left)- CSS props inside
style: {}, not top level - Related moving elements wrapped in
group() dashis SVG string ("5 5"), not CSS ("6,4")between/placeBetweenelements fit in the gap