Skip to main content
resolveElementAffordances answers the question “what can the user do with this element right now?” given a live DOM element and its SDK model entry. It returns two objects: capabilities (boolean flags for each edit action) and sections (which inspector panels apply). You use these to conditionally render controls in your editor’s detail panel rather than showing a fixed field set for all elements.
This API lives on the @hyperframes/sdk/editing subpath, available since @hyperframes/sdk@0.7.22. If the import fails to resolve, upgrade: npm install @hyperframes/sdk@latest.

Import

import { resolveElementAffordances } from "@hyperframes/sdk/editing";

Signature

function resolveElementAffordances(
  liveEl: HTMLElement,
  modelEl: Pick<HyperFramesElement, "text" | "animationIds" | "start"> | null,
  ctx?: AffordanceContext,
): EditingAffordances
  • liveEl — a live, laid-out HTMLElement from the composition iframe. The resolver calls getComputedStyle on it, so it must be attached to a rendered document.
  • modelEl — the SDK model element (comp.getElement(id)), or null when the element exists in the live DOM but not in the SDK source model (generated elements). Passing null restricts affordances: style editing is disabled and reasonIfDisabled is set.
  • ctx — optional context for studio-specific concepts. For most custom editors, omit it or leave all flags at their defaults (false).

AffordanceContext

interface AffordanceContext {
  isCompositionHost?: boolean;      // default false
  isCompositionRoot?: boolean;      // default false
  isInsideLockedComposition?: boolean; // default false
  isMasterView?: boolean;           // default false
}
These flags are studio-specific. In a standalone custom editor you can omit ctx entirely.

Return value

interface EditingAffordances {
  capabilities: DomEditCapabilities;
  sections: EditingSectionApplicability;
}

capabilities

interface DomEditCapabilities {
  canSelect: boolean;
  canEditStyles: boolean;
  /** Directly editable authored left/top style fields. */
  canMove: boolean;
  /** Directly editable authored width/height style fields. */
  canResize: boolean;
  /** Canvas translate-drag maps here, not to canMove. */
  canApplyManualOffset: boolean;
  canApplyManualSize: boolean;
  canApplyManualRotation: boolean;
  /** Set when canEditStyles / canMove / canResize is false; explains why. */
  reasonIfDisabled?: string;
}
canMove and canResize indicate that the element has authored left/top/width/height style values that can be directly edited as fields. A canvas drag operation maps to canApplyManualOffset, not canMove. Do not gate drag handles on canMove.

sections

interface EditingSectionApplicability {
  text: boolean;         // true when the element has editable text content
  media: boolean;        // true for <video> and <audio>
  colorGrading: boolean; // true for <video> and <img> — element-level only
  timing: boolean;       // true when data-start is present or animationCount > 0
  animation: boolean;    // true when animationCount > 0
}
sections.colorGrading is element-level only: it tells you this particular element supports color grading controls. Whether to show the color grading panel at all is still governed by your own feature flag — AND the two conditions together.

End-to-end example

import { openComposition, createIframePreviewAdapter } from "@hyperframes/sdk";
import { resolveElementAffordances } from "@hyperframes/sdk/editing";

const iframe = document.querySelector<HTMLIFrameElement>("#composition-frame")!;
const preview = createIframePreviewAdapter(iframe, (op) => comp.dispatch(op));
const comp = await openComposition(compositionHtml, { preview });

function onElementSelected(hfId: string) {
  // 1. Get the live DOM element from the iframe.
  const liveEl = iframe.contentDocument?.querySelector<HTMLElement>(
    `[data-hf-id="${hfId}"]`,
  );
  if (!liveEl) return;

  // 2. Get the SDK model element — null for script-generated elements.
  const modelEl = comp.getElement(hfId);

  // 3. Resolve affordances.
  const { capabilities, sections } = resolveElementAffordances(liveEl, modelEl);

  // 4. Render controls conditionally.
  renderInspector({ capabilities, sections, hfId });
}

function renderInspector({
  capabilities,
  sections,
  hfId,
}: {
  capabilities: ReturnType<typeof resolveElementAffordances>["capabilities"];
  sections: ReturnType<typeof resolveElementAffordances>["sections"];
  hfId: string;
}) {
  // Show a disabled-state tooltip when editing is locked.
  if (!capabilities.canEditStyles && capabilities.reasonIfDisabled) {
    showDisabledBanner(capabilities.reasonIfDisabled);
    return;
  }

  // Style panel — always show when editing is allowed.
  if (capabilities.canEditStyles) {
    showStylePanel(hfId);
  }

  // Position/size fields — only for absolutely positioned elements with px values.
  if (capabilities.canMove) {
    showPositionFields(hfId);
  }
  if (capabilities.canResize) {
    showSizeFields(hfId);
  }

  // Canvas drag handles — use canApplyManualOffset, NOT canMove.
  if (capabilities.canApplyManualOffset) {
    showDragHandles(hfId);
  }

  // Section panels.
  if (sections.text) showTextPanel(hfId);
  if (sections.media) showMediaPanel(hfId);
  if (sections.colorGrading && myFeatureFlags.colorGrading) showColorGradingPanel(hfId);
  if (sections.timing) showTimingPanel(hfId);
  if (sections.animation) showAnimationPanel(hfId);
}

Gotchas

Browser-only. resolveElementAffordances calls getComputedStyle internally. It must not be imported or called in Node, a server action, or any non-browser path. The SDK adapter (@hyperframes/sdk/editing) is separate from the pure core resolver precisely to keep the Node SDK path free of browser globals. Needs a laid-out DOM. canMove and canResize require a live computed position, left, and top. If you call resolveElementAffordances before the iframe finishes layout (e.g. synchronously after srcdoc assignment), both flags will be false. Wait for the iframe’s load event or the composition’s first rendered frame before resolving affordances. null model means select-only. When modelEl is null (the element is generated at runtime and has no source entry), the resolver sets existsInSource: false. This gives canSelect: true but canEditStyles: false, canMove: false, and all manual-geometry flags false, with reasonIfDisabled set to a human-readable explanation. You can still show the selection highlight; just hide or disable all edit controls. canApplyManualOffset vs canMove. canMove is true only when the element has absolute/fixed positioning AND authored left/top px values AND no transform-driven geometry. canApplyManualOffset is true for all non-root non-host elements regardless of positioning. A translate-based canvas drag (the applyDraft / commitPreview flow in Canvas Integration) uses canApplyManualOffset as its gate — it does not require canMove. colorGrading is element-level, not feature-level. sections.colorGrading tells you the element type supports grading (<video> or <img>). Your own feature flag is a separate AND condition. Never replace your feature flag with this field.

Type reference

The full type definitions — EditingAffordances, DomEditCapabilities, EditingSectionApplicability, AffordanceContext, HyperFramesElement — are documented at /sdk/reference/types. The pure DOM-free resolver (resolveEditingAffordances, EditableElementFacts) lives in @hyperframes/core and is documented at /packages/core.

Canvas Integration

Get the live element and selection via hit-testing and the iframe adapter.

Types reference

Complete type definitions for the SDK public surface.