---
title: RFC — Eidos shape engine
type: rfc
audience: human + agent
status: implemented — phases 1-5 ✅
source: migrated from src/uix/eidos/SHAPE_ENGINE_RFC.md (2026-07-02, docs-book F7.4)
---
# RFC — Eidos shape engine
> Sibling of [`rfc-color-engine.md`](./rfc-color-engine.md),
> [`rfc-typography.md`](./rfc-typography.md) and [`rfc-depth.md`](./rfc-depth.md).
> Takes the **shape** channel (`shape` from the book _Diseñando lo que ocurre_)
> to reference-grade by **breaking** the references' model — with the **open
> cage**. It is the book's **8th and last expression channel** left to elevate
> (time · motion · presence · depth · **shape** · color · sound · haptic).
## 0. Thesis
> **Shape is not a rounding number an element _has_; it is a perceptual quality
> — continuity, family, tension — that the system _composes_ and that, in its
> moment, _happens_.**
Everybody reduces shape to ONE number: `border-radius` . But the corner has more
axes than its magnitude: **how continuous** it is (circular arc vs
superellipse), which **family** it belongs to (rounded / continuous / cut /
pill), how it **relates** to nested shapes, and **what happens to it** when the
element is pressed or commits. Eidos treats shape as a **first-class channel** :
unified (magnitude + continuity + family + harmony), and **eventful** (shape can
tense/relax), under the **open cage** .
## 1. The survey — how the references do it and where they stop
| Framework | Model | Structural limit |
| ------------------------ | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| **Tailwind / Bootstrap** | `rounded-sm…full` scale, circular arc | no continuity, no nested harmony, no family, static; the corner is an arc, period |
| **Material 3** | shape scale + per-component mapping + (M3 Expressive) _morph_ + `cut` family | arc/cut; the morph is recent and narrow; a **closed** system (you pick from the scale) |
| **Apple / SwiftUI** | **continuous** corners (superellipse) `.continuous` | platform-bound, not a portable token; not eventful; manual nesting |
| **CSS today** | `border-radius` = elliptical arcs | no universal native superellipse (`corner-shape` emerging); squircles via SVG/clip-path hacks |
**Common limit**: shape = **one arc-radius number** , a **static** property you
assign, **detached** from neighboring/nested elements, without real
**continuity**, and **disconnected from what happens** . SwiftUI is the only one
with continuity — but closed and per-platform. Material is the only one with
family + morph — but arc-based and closed. Nobody **unifies** them or makes
them **open** .
## 2. Where Eidos stands today (coherent, but not novel)
- **Radius scale** in `--radius-{none·sm·md·lg·xl·xxl·full}` (4/6/10/16/20px + pill). ✓
- Assigned **per component** via the size primitive (`radius: 'md'`). ✓
- **BUT** the model is exactly "one **circular arc** number": no continuity
(squircle), no **harmony** between nested radii, no perceptual **family** , and
**static** (it never happens). You are ~on par with Tailwind; below SwiftUI
(continuity) and M3 (family + morph).
## 3. The novel model — 4 pieces
1. **Continuity (superellipse)** — a new axis `--shape-smoothing` (0 = circular
arc … 1 = iOS-style superellipse). **Progressive output** (the exact strategy
of color's oklch): `border-radius` is emitted (universal, the fallback)
**and** `corner-shape: superellipse(…)` where supported. Solves at the root
the arc-corner everyone drags along, breaking nobody.
2. **Nested-radius harmony** — inner radius **derived** from the outer:
`inner = max(0, outer − gap)` (concentric), computed in `calc()` . A
`--shape-nest` convention so card-inside-card (or input-inside-panel) never
misaligns.
3. **Shape family** — `rounded · continuous · cut · pill` (+ `sharp` ). A
**perceptual** axis, not one of magnitude. Eidos canon (like roles/variants):
themeable values, canonical set.
4. **_Eventful_ shape (the genuinely new part)** — shape **tenses/relaxes** when
something happens: `contact` →squeezes corners, `commit` →rounds them, via the
**same signature** that already drives motion/depth. Shape-as-happening,
coordinated from **one single sema event** .
## 3.bis The two moments of shape
Shape respects the framework's **two-moments model** (motion F1) — it does not
reinvent it:
| Moment | Attribute (axis) | For shape | Phase |
| --------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ----- |
| **state** | `data-state` (persistent) | the **resting shape** — magnitude (`--radius-*`) + family + continuity: how the element _is_ | 1 |
| **event** | `data-event-*` (transient, during the `hold` ) | the **morph** — squeeze/round on `contact` /`commit`: what _happens_ . Ordered by `sequence` , next to the motion/depth signatures | 3 |
A control **has** a shape (state) **and tenses it** when pressed (event) — the
two moments, never collapsed. The book's same rigor (event ≠ state) carried to
shape.
## 4. Doctrine — _strong default, open cage_
| Piece | Strong default | Open door |
| ---------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **scale** | canonical `--radius-*` | config-driven (`EidosConfig` size/radius); raw `border-radius` always one step away |
| **continuity** | `--shape-smoothing` on by default (subtle) | global/per-component/per-element override; `0` returns to the pure arc |
| **family** | canonical set (rounded/continuous/cut/pill/sharp) | the set is **canon** but themeable; a wrapper composes its own without inventing a family |
| **nesting** | `--shape-nest` convention (concentric) | opt-in; the computation is transparent (`calc()`), overridable |
| **eventful** | `contact` ↓squeeze / `commit` ↑round | **opt-in/opt-out** (like sound/haptic/depth); sema's open registry; **degrades with `prefers-reduced-motion`** ; static shape works without the channel |
| **whole system** | canonical theme | runtime ** `applyShape(seed)` ** (sibling of `applyColorScheme` /`applyTypeScale`/`applyDepth`) |
It is exactly how color/typography/depth already operate: retintable canon +
runtime builders + primitives always reachable. Shape inherits the same
openness contract.
## 5. Token contract (additions, frozen)
Bare-prefixed by system (`--shape-…`). The **magnitude** stays in `--radius-*`
(nothing renamed → zero breakage):
```
--radius-{key} magnitude (existing scale — kept)
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress)
Re-audit of the whole component catalog at pilot depth (91 fichas + the
checkpoint verdicts in docs/audit/components/) and the executed fix packages.
- P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 +
API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d,
component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts
(role=application removed ×4, aria-selected off the Day, drp translationRef,
field data-state prune, pin-input commit-set, media-player renames), 13 new
sema packs + 12 morfos family-default → pack.
- P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar);
onValueCommit terminal-callback norm (pin-input/search/password/textarea +
date/time/color-field add); typed validation reason + onInvalid (tags-input,
css-field); index/onIndexChange (carousel); deselectable; openDelay/
groupSkipDelay; allowCustomValue; defaultValue prune.
- P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across
cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in
field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel);
touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual,
44 AAA). S6 (Field composition) + S8 (calendar-surface) pending.
Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline);
morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched
components; per-component vitest suites green.
Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md
Excluded (broken by the N1 rename, left broken per user decision, not staged):
words/**, palabras/**, chronos, web/routes/alpha/**.
Reconciliation pending: the touch-rows ::before for checkbox/switch reverses
changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed
markers to a labeled-row/Field task — flagged for the user in the handoff.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
--shape-smoothing superellipse EXPONENT (1 = circular arc, 2 ≈ the iOS
squircle — the shipped semantics, `STATIC_SHAPE` ;
this RFC's earlier "factor 0..1" draft did not land)
→ corner-shape: superellipse(k)
--shape-nest-gap default gap for the nested radius (Phase 2)
```
- **Continuity**: `[style] { border-radius: var(--radius-md) }` (universal) +
where supported `corner-shape: superellipse(var(--shape-smoothing-k))`
(progressive, degrades to the arc).
- **Family**: `data-shape='cut'|'continuous'|…` attribute or the component's
recipe (not a global token) — selects the appropriate `corner-shape` / clip.
- **Nesting**: `border-radius: max(0px, calc(var(--_outer-radius) -
var(--shape-nest-gap)))` — a `calc()` convention, no new output token.
> **4-corner nesting requires FINITE radii — at `full` only the _top_ nests.**
> At `full` (`--radius-full` = 9999px) the radius is **clipped to half the
> smaller dimension of each element**. A wide child (e.g. 16:10) **cannot** be
> concentric on all 4 corners: its height caps all four at ~½-height, different
> from the parent. But its **top corners CAN** be `card_radius − gap` if the
> **bottom ones stay square** (the height constraint stops biting). That is the
> iOS player geometry: artwork with a concentric rounded top + a straight base
> meeting the metadata.
>
> The `[data-shape-nest]` CSS rule does not work here because the parent's
> _computed_ radius value is still `9999px` (the cap is a _used_ value, not
> readable from CSS). The `/temas/forma` demo **measures** it
> (`min(w,h)/2 − gap` via `ResizeObserver`) and applies it to the artwork's top
> corners; the bottom ones stay at `0`. The "concentric radii" toggle is
> disabled at `full` ("auto: top only") because there the nesting is automatic
> and partial by geometry, not optional.
## 6. Phases (the flexibility is baked into each)
1. ✅ **Continuity + scale + families** — `--shape-smoothing` + `data-shape` →
`corner-shape` with the 4 families (`rounded`/`continuous`/`cut`/`scoop`).
Progressive emission: universal `border-radius` (the magnitude, `--radius-*`
intact) + `corner-shape` where supported (degrades to the arc). Config-driven
- validation + test + regen. (Families were brought forward here — they are
the same `corner-shape` mechanic.)
2. ✅ **Nested harmony** — `--shape-nest-gap` + `[data-shape-nest]` rule:
`border-radius: max(0, var(--shape-outer-radius) − gap)` (concentric).
3. ✅ **_Eventful_ shape (event moment)** — the corner morph **folded into the
`press-squeeze` signature** (cross-modal: one press = scale + shadow +
corner) + `@property --shape-smoothing` so it interpolates. On the existing
`signatures` system; degrades with reduced-motion. Not a parallel system.
4. ✅ **Runtime builder `applyShape(seed)`** — `smoothing` /`nestGap` dial +
family override, managed block (sibling of
`applyColorScheme` /`applyTypeScale`/`applyDepth`).
5. ✅ **Showcase + docs** — `/temas/forma` (continuity with a dial + families +
nesting + eventful, live) + theming reference §30.
## 7. Composition with the existing
- **`--radius-*` scale** → each shape's magnitude (reused, not duplicated).
- **`signatures` system** (motion/depth) → the eventful morph (Phase 3), not a
new engine.
- **Motion's two moments** → the morph is a movement of shape, not a jump.
- **Sema event bus** → the eventful trigger (Phase 3), like sound/haptic/depth.
- **Spacing** → the nested-radius gap (Phase 2).
Nothing is reinvented: what is today a loose number gets **unified + made
continuous, harmonic and eventful**, under the book's doctrine.
## 8. Doctrine (parallel to color / typography / depth)
- **Scale + families = eidos canon** (like roles/variants and planes): themeable
values, but the _set_ is canon.
- **Continuity = a default quality**, retunable; `0` = pure arc (open cage).
- **Eventful shape = an engine capability**, opt-in with degradation (like
sound/haptic/depth).
- **Theme = retint/retune the perceptually fixed**: change HOW MUCH `md` rounds
or how much continuity it carries, not WHAT `cut` means.
- **Open cage**: a strong opinion that never traps — raw `border-radius` always
reachable.
## 9. Out of scope
- Blobs / random organic shapes and arbitrary `path` morphing (shape is
perceptual, not a free-geometry engine).
- Complex geometry clipping (bespoke per-component clip-path stays in each recipe).
- Asymmetric per-side corners as a system (raw per-corner `border-radius`
already covers them).