M3: an instance lists the properties its variant uses; long property names stay on one line (#2022)

In Figma's data a component set holds one text property per variant (plus leftovers of deleted variants), and an instance lists only the ones its current variant's layers read (measured in Figma's web app). FigCraft listed every definition on every instance, and wrapped a long property name inside an 84 x 20 box so its lines painted over the rows around it.

- components::instance_prop_list: prop_list keeping slots and the properties a layer of the component references (a nested instance counts, its layers and slot content are not entered). component.properties and the Properties panel use it for instances; sets, components and variants still list every definition.
- prop_label cuts the name to one line with an ellipsis and shows the full name on hover.
- Tests: an_instance_lists_only_the_properties_its_variant_uses, a_long_property_name_stays_on_one_line, and the pin a_set_keeps_each_named_text_definition_and_layers_read_their_own. Two tests and one setup that listed an unbound boolean on an instance now bind it to a layer.
- docs/components-parity.md: the properties row, header and revision history.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Fable 5.1 briefed; Sonnet 5.5 wrote at medium
This commit is contained in:
Trevor Brown
2026-11-11 14:42:37 -05:00
co-authored by Claude Sonnet 5.5
parent 74698fa89f
commit 14670b99d4
9 changed files with 140 additions and 13 deletions
+24
View File
@@ -298,6 +298,30 @@ pub fn prop_list(doc: &Document, c: NodeId) -> Vec<(String, PropDef)> {
}
}
/// The properties an instance of `component` lists: [`prop_list`] without the boolean, text and
/// instance swap properties that no layer of the component reads. A set's variants share its
/// definitions, so one a variant's layers don't bind (another variant's text, a leftover of a
/// deleted variant) stays defined, and shows on the set, but not on this variant's instances
/// (#2022). Slots stay. Walks as [`slots_of`] does: a nested instance counts, its own layers and
/// the content put into slots are not entered.
pub fn instance_prop_list(doc: &Document, component: NodeId) -> Vec<(String, PropDef)> {
let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut stack = vec![component];
let mut seen = 0usize;
while let Some(c) = stack.pop() {
seen += 1;
if seen > 1_000_000 {
break;
}
let Some(n) = doc.node(c) else { continue };
used.extend(n.prop_refs.values().cloned());
if c == component || (!is_instance(doc, c) && !holds_content(n)) {
stack.extend(n.children.iter().rev().copied());
}
}
prop_list(doc, component).into_iter().filter(|(k, d)| !takes_value(d) || used.contains(k)).collect()
}
/// Whether instances store a value for the property (every kind but slots).
pub fn takes_value(d: &PropDef) -> bool {
!matches!(d, PropDef::Slot { .. })
+4 -1
View File
@@ -395,8 +395,11 @@ fn unbind_delete_and_vary(s: &mut Session, c: NodeId, i: NodeId, title: NodeId,
ex(s, "component.deleteProperty", json!({"id": c, "name": "Heading"}));
assert!(node(s, title).prop_refs.is_empty());
assert!(matches!(&node(s, i).kind, NodeKind::Instance { props, .. } if !props.contains_key("Heading")));
let desc = ex(s, "component.properties", json!({"id": i}));
// "Show dot" is still defined but no layer reads it any more: the main lists it, the instance doesn't (#2022).
let desc = ex(s, "component.properties", json!({"id": c}));
assert_eq!(desc["properties"].as_array().unwrap().len(), 1);
let desc = ex(s, "component.properties", json!({"id": i}));
assert!(desc["properties"].as_array().unwrap().is_empty());
// A variant property on a lone component makes it a component set.
let set = idv(&ex(s, "component.addProperty", json!({"id": c, "name": "Theme", "type": "variant", "default": "Light"})));
assert!(matches!(node(s, set).kind, NodeKind::ComponentSet { .. }));
+3 -2
View File
@@ -8,7 +8,7 @@ use super::{
req_string, req_targets, set_boolean_default, slot_edited, slots_of, string, takes_value, targets, variant_conflicts, variant_name,
variant_options, variant_props, variant_set,
};
use crate::components::prop_list;
use crate::components::{instance_prop_list, prop_list};
/// Whether instance `id` has a component property `name` that a `kind` variable can drive (a
/// boolean property takes a boolean variable, a text or variant property a string one).
@@ -606,7 +606,8 @@ pub(crate) fn describe_props(doc: &Document, id: NodeId) -> Value {
}
// Slots: the frames (on a main) or this instance's copy, with whether it holds its own content.
let slots = slots_of(doc, if values.is_some() { id } else { variant_set(doc, comp).unwrap_or(comp) });
for (k, d) in prop_list(doc, comp) {
let defs = if values.is_some() { instance_prop_list(doc, comp) } else { prop_list(doc, comp) };
for (k, d) in defs {
let mut v = def_json(&k, &d);
if !takes_value(&d) {
let layers: Vec<NodeId> = slots.iter().filter(|(_, n)| *n == k).map(|(l, _)| *l).collect();
+26 -1
View File
@@ -77,8 +77,9 @@ fn every_kind_of_property_keeps_its_place_on_rename() {
let icon = idv(&ex(&mut s, "component.create", json!({"ids": [icon_f]})));
let nested = idv(&ex(&mut s, "instance.create", json!({"component": icon, "x": 60, "y": 10, "parent": f})));
let small = idv(&ex(&mut s, "component.create", json!({"ids": [f]})));
let zoom_layer = idv(&ex(&mut s, "shape.rectangle", json!({"x": 150, "y": 10, "width": 10, "height": 10, "parent": small})));
// Made in an order that isn't by name: Zoom (boolean), Label (text), Glyph (instance swap), Body (slot).
ex(&mut s, "component.addProperty", json!({"id": small, "name": "Zoom", "type": "boolean"}));
ex(&mut s, "component.addProperty", json!({"id": small, "name": "Zoom", "type": "boolean", "layer": zoom_layer}));
ex(&mut s, "component.addProperty", json!({"id": small, "name": "Label", "type": "text", "layer": label}));
ex(&mut s, "component.addProperty", json!({"id": small, "name": "Glyph", "type": "instanceSwap", "layer": nested}));
ex(&mut s, "component.makeSlot", json!({"id": body}));
@@ -133,3 +134,27 @@ fn make_slot_takes_the_name_typed() {
by_name.sort();
assert_eq!(listed, by_name);
}
/// An instance lists the properties its own variant's layers use (#2022): in Figma's data a set
/// has one text property per variant (and leftovers of deleted ones), measured: an instance of
/// variant A lists `State` and `A label`, never `B label` or a property no layer reads.
#[test]
fn an_instance_lists_only_the_properties_its_variant_uses() {
let mut s = Session::new();
let mut variants = Vec::new();
let mut texts = Vec::new();
for (k, name) in ["Type=Flag, State=Default", "Type=Flag, State=Selected"].iter().enumerate() {
let f = idv(&ex(&mut s, "frame.create", json!({"x": 0, "y": k * 60, "width": 120, "height": 40, "name": name})));
texts.push(idv(&ex(&mut s, "text.create", json!({"text": "Label", "x": 4, "y": 4, "parent": f, "name": "Label"}))));
variants.push(idv(&ex(&mut s, "component.create", json!({"ids": [f]}))));
}
let set = idv(&ex(&mut s, "component.combineVariants", json!({"ids": variants})));
ex(&mut s, "component.addProperty", json!({"id": set, "name": "Default label", "type": "text", "layer": texts[0]}));
ex(&mut s, "component.addProperty", json!({"id": set, "name": "Selected label", "type": "text", "layer": texts[1]}));
ex(&mut s, "component.addProperty", json!({"id": set, "name": "Gone label", "type": "text"}));
assert_eq!(names(&mut s, set), ["Type", "State", "Default label", "Selected label", "Gone label"], "the set keeps every definition");
let inst = idv(&ex(&mut s, "instance.create", json!({"component": variants[0], "x": 400, "y": 0})));
assert_eq!(names(&mut s, inst), ["Type", "State", "Default label"]);
ex(&mut s, "instance.setProperties", json!({"ids": [inst], "properties": {"State": "Selected"}}));
assert_eq!(names(&mut s, inst), ["Type", "State", "Selected label"]);
}
+40
View File
@@ -186,3 +186,43 @@ fn property_values_written_only_as_variables_are_applied() {
assert_eq!(props.get("Hint"), Some(&Value::Bool(false)));
assert_eq!(props.get("Label"), Some(&Value::String("Name on card".into())));
}
/// #2022: a set keeps each named text definition under its own name (Figma writes one per variant,
/// plus leftovers of deleted variants), and each variant's text reads its own copy.
#[test]
fn a_set_keeps_each_named_text_definition_and_layers_read_their_own() {
let text = |chars: &str| o(vec![("textValue", o(vec![("characters", s(chars))]))]);
let names = ["Type=Flag / State=Default label", "Type=Flag / State=Selected label", "Type=Issue / State=Default label"];
let set_defs: Vec<Kv<'static>> =
names.iter().zip([70, 71, 72]).zip(["One", "Two", "Gone"]).map(|((n, id), c)| definition(id, n, "TEXT", text(c))).collect();
let copies = |first: u32| Kv::Array((0..3).map(|k| o(vec![("id", g(first + k)), ("parentPropDefId", g(70 + k))])).collect());
let msg = message(vec![
node(9, "FRAME", "Chip", Some(2), vec![("isStateGroup", Kv::Bool(true)), ("componentPropDefs", Kv::Array(set_defs))]),
node(10, "SYMBOL", "Type=Flag, State=Default", Some(9), vec![("componentPropDefs", copies(80))]),
node(11, "TEXT", "Label", Some(10), vec![bound("TEXT_DATA", 80)]),
node(20, "SYMBOL", "Type=Flag, State=Selected", Some(9), vec![("componentPropDefs", copies(90))]),
node(21, "TEXT", "Label", Some(20), vec![bound("TEXT_DATA", 91)]),
]);
let (rest, _, _) = to_rest(&msg, "Synthetic", crate::rest::MAX_NODES);
let defs = &by_id(&rest["document"], "9:9")["componentPropertyDefinitions"];
for (k, n) in names.iter().enumerate() {
assert_eq!(defs[format!("{n}#9:{}", 70 + k)]["type"], "TEXT", "{n}");
}
assert_eq!(defs.as_object().map(|d| d.len()), Some(3), "{defs}");
assert_eq!(by_id(&rest["document"], "9:11")["componentPropertyReferences"]["characters"], format!("{}#9:70", names[0]));
assert_eq!(by_id(&rest["document"], "9:21")["componentPropertyReferences"]["characters"], format!("{}#9:71", names[1]));
let (doc, _) = crate::rest::import_fig(&rest, &crate::rest::no_images).unwrap();
let refs: Vec<Option<String>> = doc
.ids()
.filter_map(|id| doc.node(id))
.filter(|n| n.name == "Label" && n.source.is_none())
.map(|n| n.prop_refs.get("characters").cloned())
.collect();
assert_eq!(refs.len(), 2, "{refs:?}");
assert!(refs.contains(&Some(names[0].to_string())) && refs.contains(&Some(names[1].to_string())), "{refs:?}");
for set in doc.ids().filter_map(|id| doc.node(id)).filter(|n| matches!(n.kind, NodeKind::Component { .. })) {
let NodeKind::Component { props, .. } = &set.kind else { continue };
assert!(names.iter().all(|n| props.contains_key(*n)), "{} holds {:?}", set.name, props.keys().collect::<Vec<_>>());
}
}
@@ -19,12 +19,19 @@ fn id(v: serde_json::Value) -> NodeId {
fn setup() -> (Ui, NodeId, NodeId) {
let mut s = Session::new();
let mut variants = Vec::new();
let mut icons = Vec::new();
for (k, name) in ["Size=Small, State=Default", "Size=Large, State=Default", "Size=Small, State=Hover"].iter().enumerate() {
let f = id(s.execute("frame.create", &json!({"x": 0, "y": k * 60, "width": 80, "height": 40, "name": name})).unwrap());
// A layer in every variant reads "Icon": an instance lists only the properties its variant uses.
let icon = id(s.execute("shape.rectangle", &json!({"x": 4, "y": 4, "width": 10, "height": 10, "parent": f})).unwrap());
icons.push(icon);
variants.push(id(s.execute("component.create", &json!({"ids": [f]})).unwrap()));
}
let set = id(s.execute("component.combineVariants", &json!({"ids": variants})).unwrap());
s.execute("component.addProperty", &json!({"id": set, "name": "Icon", "type": "boolean", "default": true})).unwrap();
s.execute("component.addProperty", &json!({"id": set, "name": "Icon", "type": "boolean", "default": true, "layer": icons[0]})).unwrap();
for icon in &icons[1..] {
s.execute("component.bindProperty", &json!({"layer": icon, "property": "Icon"})).unwrap();
}
let inst = id(s.execute("instance.create", &json!({"component": set, "x": 400, "y": 0})).unwrap());
s.execute("select.set", &json!({"ids": [set]})).unwrap();
(Ui::new(s), set, inst)
@@ -123,3 +130,25 @@ fn escape_cancels_a_property_drag_and_keeps_the_selection() {
assert_eq!(undo_len(&u), undo);
assert_eq!(u.app.session.ds().unwrap().selection, [set], "the Escape only cancels the drag");
}
/// A long property name is cut to one line inside its label box instead of wrapping over the rows
/// above and below (#2022); the name itself stays whole.
#[test]
fn a_long_property_name_stays_on_one_line() {
let long = "Flag Default label with a long name";
let mut s = Session::new();
let f = id(s.execute("frame.create", &json!({"x": 0, "y": 0, "width": 120, "height": 40, "name": "Chip"})).unwrap());
let t = id(s.execute("text.create", &json!({"text": "Label", "x": 4, "y": 4, "parent": f})).unwrap());
let c = id(s.execute("component.create", &json!({"ids": [f]})).unwrap());
s.execute("component.addProperty", &json!({"id": c, "name": long, "type": "text", "layer": t})).unwrap();
let inst = id(s.execute("instance.create", &json!({"component": c, "x": 400, "y": 0})).unwrap());
s.execute("select.set", &json!({"ids": [inst]})).unwrap();
let mut u = Ui::new(s);
let texts = crate::export_name_tests::painted(&mut u);
let panel = u.app.ui.right_panel_rect.expect("the right panel shows");
let labels: Vec<(String, egui::Rect)> = texts.into_iter().filter(|(t, r)| t.starts_with("Flag") && r.min.x >= panel.min.x).collect();
let [(text, rect)] = labels.as_slice() else { panic!("one property label: {labels:?}") };
assert!(text.ends_with('…'), "cut with an ellipsis, not {text:?}");
assert!(rect.height() <= crate::widgets::FIELD_H, "one line, not {} tall", rect.height());
assert_eq!(names(&mut u, inst), [long], "the name itself stays whole");
}
+1 -1
View File
@@ -10,7 +10,7 @@ use crate::layers_drag_tests::Ui;
use crate::widgets::FIELD_H;
/// The text painted this frame, with each piece's box.
fn painted(u: &mut Ui) -> Vec<(String, Rect)> {
pub(crate) fn painted(u: &mut Ui) -> Vec<(String, Rect)> {
fn walk(shape: &Shape, out: &mut Vec<(String, Rect)>) {
match shape {
Shape::Text(t) => out.push((t.galley.text().to_string(), t.visual_bounding_rect())),
+9 -5
View File
@@ -14,8 +14,8 @@ use egui::{Id, Sense, Ui, vec2};
use figcraft_doc::{Document, Node, NodeId, NodeKind, PropDef, VariableType};
use figcraft_engine::cmd::component::{can_add_variant, can_combine_variants};
use figcraft_engine::components::{
PROP_BINDING, default_variant, inside_instance, owning_component, owning_instance, prop_list, slot_def, slot_edited, slots_of, variant_conflicts,
variant_options, variant_props, variant_set,
PROP_BINDING, default_variant, inside_instance, instance_prop_list, owning_component, owning_instance, prop_list, slot_def, slot_edited,
slots_of, variant_conflicts, variant_options, variant_props, variant_set,
};
use serde_json::{Value, json};
@@ -748,7 +748,7 @@ fn instance_props(app: &mut FigApp, ui: &mut Ui, doc: &Document, n: &Node, comps
}
}
// Boolean, text and instance-swap properties.
for (k, d) in prop_list(doc, *component) {
for (k, d) in instance_prop_list(doc, *component) {
let v = props.get(&k).cloned().unwrap_or_else(|| figcraft_engine::components::prop_default_value(&d));
row(ui, 32.0, |ui| {
prop_label(ui, &k, type_icon(&d));
@@ -968,8 +968,12 @@ fn comp_rows(ui: &mut Ui, found: &CompSearch, checked: &impl Fn(NodeId) -> bool)
fn prop_label(ui: &mut Ui, name: &str, icon: Icon) {
let t = Tokens::get(ui.ctx());
let (r, _) = ui.allocate_exact_size(vec2(84.0, 20.0), Sense::hover());
let (r, resp) = ui.allocate_exact_size(vec2(84.0, 20.0), Sense::hover());
icons::paint(ui.painter(), egui::Rect::from_min_size(egui::pos2(r.min.x, r.center().y - 6.0), vec2(12.0, 12.0)), icon, t.icon_secondary);
let g = ui.painter().layout(name.to_string(), crate::theme::regular(11.0), t.text_secondary, r.width() - 18.0);
let shown = widgets::elide(ui, name, r.width() - 18.0);
let g = ui.painter().layout_no_wrap(shown.clone(), crate::theme::regular(11.0), t.text_secondary);
ui.painter().galley(egui::pos2(r.min.x + 18.0, r.center().y - g.size().y / 2.0), g, t.text_secondary);
if shown != name {
resp.on_hover_text(name);
}
}
+3 -2
View File
@@ -1,6 +1,6 @@
# Components, variants, styles and variables parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** minor (the Local variables window selects several variables (click, Shift+click for a range, Cmd/Ctrl+click to toggle, Escape to clear) and delete, move to a group and scope act on the selected local variables in one undo step (#2002); text styles carry paragraph spacing and indent, the list style and the OpenType features, as the Supported in text styles table of Create and apply text styles says (#1829); a layer's effect binding takes only a field its effect has; Go to main component on an instance of a library component opens the library file in a tab and selects the main there; glass effects take variables; Delete on a layer inside an instance hides it as an override instead of being refused, as the help center says: "Figma will only toggle the layer's visibility instead of removing it"; library analytics list how many layers and files use each style and variable; SwiftUI and Compose code names a bound variable by its iOS or Android code syntax; library analytics count insertions and show insertions and detachments over the past 30 days, 90 days or year; a collection of this file can be extended (Extend collection on its row, `variable.extendCollection`), its extension following its edits at once, and extended collections extend again in the file; a typed or stored variable, collection or mode id names only the object with that id (#1613); colour variables can be calculated from other colours per mode — `{alpha}` replaces a colour's alpha, `{mix}` blends two — and follow them (#1280); stored variable and collection references keep their identity when another object is named like their id (follow-up to #1542); a component set's property rows fit the 240 px right panel: variant values wrap two to a line there and every + and − stays in view; opening a file contacts no team library server it names until you check it (#1097); an explicit variables file path (`variable.import`, the `variables` of `file.open` and `file.import`) is refused past 256 MiB or when it isn't a regular file, as the sibling `variables.json` already was (part of #1114); component properties keep the order they were made in when one is renamed, and Make slot asks for the slot's name first (#1536, #1537); components can't contain their own instances, directly or through others (#1084); the instance-swap menus have a search field (#1338); an instance-swap property's default menu scrolls (#1338); variables on a text style's fields from the style editor; instance-swap properties in the component playground; duplicating or Alt-dragging a main component makes an instance of it (#892); a bound colour variable's alpha applies once (#802); variables import reads `variables` outside `meta` and warns when it imports none (#790); variant properties reorder by drag; library files saved right after a read are read again; Hide when publishing note corrected; size and resizing overrides of layers inside instances (#731); variables export in the browser as a download; core editor leftovers: component playground, variables inside styles; layers and assets panels behaviour suite and fixes; extended collections tail; styles and variables behaviour suite and fixes; components behaviour suite and fixes; team libraries on the server, library usage and swapping a library; extended collections, variables import/export, gradient stops and variant properties bound to variables) · **Target:** Figma Design (desktop app 126.10.7)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-11 · **Change:** minor (an instance lists only the properties its variant's layers use and long property names are cut to one line (#2022); the Local variables window selects several variables (click, Shift+click for a range, Cmd/Ctrl+click to toggle, Escape to clear) and delete, move to a group and scope act on the selected local variables in one undo step (#2002); text styles carry paragraph spacing and indent, the list style and the OpenType features, as the Supported in text styles table of Create and apply text styles says (#1829); a layer's effect binding takes only a field its effect has; Go to main component on an instance of a library component opens the library file in a tab and selects the main there; glass effects take variables; Delete on a layer inside an instance hides it as an override instead of being refused, as the help center says: "Figma will only toggle the layer's visibility instead of removing it"; library analytics list how many layers and files use each style and variable; SwiftUI and Compose code names a bound variable by its iOS or Android code syntax; library analytics count insertions and show insertions and detachments over the past 30 days, 90 days or year; a collection of this file can be extended (Extend collection on its row, `variable.extendCollection`), its extension following its edits at once, and extended collections extend again in the file; a typed or stored variable, collection or mode id names only the object with that id (#1613); colour variables can be calculated from other colours per mode — `{alpha}` replaces a colour's alpha, `{mix}` blends two — and follow them (#1280); stored variable and collection references keep their identity when another object is named like their id (follow-up to #1542); a component set's property rows fit the 240 px right panel: variant values wrap two to a line there and every + and − stays in view; opening a file contacts no team library server it names until you check it (#1097); an explicit variables file path (`variable.import`, the `variables` of `file.open` and `file.import`) is refused past 256 MiB or when it isn't a regular file, as the sibling `variables.json` already was (part of #1114); component properties keep the order they were made in when one is renamed, and Make slot asks for the slot's name first (#1536, #1537); components can't contain their own instances, directly or through others (#1084); the instance-swap menus have a search field (#1338); an instance-swap property's default menu scrolls (#1338); variables on a text style's fields from the style editor; instance-swap properties in the component playground; duplicating or Alt-dragging a main component makes an instance of it (#892); a bound colour variable's alpha applies once (#802); variables import reads `variables` outside `meta` and warns when it imports none (#790); variant properties reorder by drag; library files saved right after a read are read again; Hide when publishing note corrected; size and resizing overrides of layers inside instances (#731); variables export in the browser as a download; core editor leftovers: component playground, variables inside styles; layers and assets panels behaviour suite and fixes; extended collections tail; styles and variables behaviour suite and fixes; components behaviour suite and fixes; team libraries on the server, library usage and swapping a library; extended collections, variables import/export, gradient stops and variant properties bound to variables) · **Target:** Figma Design (desktop app 126.10.7)
Design systems are why teams adopt Figma. **Components: breadth ~96%, ready ~72%. Styles and
variables: breadth ~95%, ready ~76% (estimated).** QA'd with a synthetic design system of ~300
@@ -12,6 +12,7 @@ accounts).
| Feature | Status | Notes |
|---|---|---|
| 2026-11-11 | minor | M3, #2022: an instance lists only the properties its variant's layers use (its variant properties, slots, and the boolean, text and instance swap properties a layer of its current variant reads), as measured in Figma's web app: an instance of variant A lists `State` and `A label`, never `B label` or a property no layer reads. The set and its variants still list every definition, so unused ones can be deleted there. A long property name in the panel is cut to one line with an ellipsis, the full name on hover, instead of wrapping over the rows around it. Tests: `prop_order_tests::an_instance_lists_only_the_properties_its_variant_uses`, `component_props_reorder_tests::a_long_property_name_stays_on_one_line`, `prop_value_tests::a_set_keeps_each_named_text_definition_and_layers_read_their_own`. Grade unchanged. |
| Create component, instances, detach, reset, go to main, restore main, push changes | done | push from a nested layer goes to the outer main |
| Per-layer overrides with reset; kept across main edits, nested swaps, copy/paste | done | main edits match a full rebuild exactly at 2,000 instances |
| What an instance overrides: fills, strokes, effects, visibility, layer names, text content and every text property (resizing included), auto layout padding, gap and alignment, layout grids, export settings, prototype interactions, nested swaps and property values; the size and resizing (hug, fill, fixed, min and max) of layers inside it | done | each kept when the main changes that property, reset per group; opacity and corner radius too (documentation silent); size and resizing are kept through relayout, undo, save, `.fig` and REST import and detach, and stay with the instance on push |
@@ -22,7 +23,7 @@ accounts).
| Switching variants keeps overrides where names match and both variants had the same value; text layers keep theirs | done | |
| Default variant: the top-left one (new instances, Assets thumbnail, generated code) | done | |
| Variant property order (names rewritten), duplicate combinations reported | done | drag a property in the Properties section to reorder it, one undo step (#679), or `component.editProperty {index}` |
| Properties: boolean, text, instance swap (preferred values), variant | done | listed in the order they were made, a rename keeping the place (#1536; the shared ones don't reorder by drag yet); every component menu scrolls past 320 pt, the default of an instance-swap property's too (#1338); the instance-swap menus (an instance's property and swap menus, a property's default, the playground) open with a search field that filters by name as you type, with Up, Down and Enter to pick and Escape to clear then close; the slot and preferred components menus have no search yet |
| Properties: boolean, text, instance swap (preferred values), variant | done | listed in the order they were made, a rename keeping the place (#1536; the shared ones don't reorder by drag yet); every component menu scrolls past 320 pt, the default of an instance-swap property's too (#1338); the instance-swap menus (an instance's property and swap menus, a property's default, the playground) open with a search field that filters by name as you type, with Up, Down and Enter to pick and Escape to clear then close; the slot and preferred components menus have no search yet; an instance lists the properties its variant's layers use, so a set with one text property per variant shows one text field per instance (#2022; measured in Figma's web app, 2026-11-11); long names are cut to one line, the full name on hover |
| Slots (preferred components, fill menu, kept across updates) | done | Figma 2025 feature; Make slot asks for the name first (a field under the frame's Slot row), and slots are renamed in the component's Properties list (#1537); no rename with only the slot's frame selected yet |
| Exposed nested instances | done | hidden ones hide their properties |
| Descriptions | done | |