Surfaces
Surfaces describe how your app renders inside Teachfloor — as a drawer that slides in from the app dock, or as a widget embedded inline in a layout. Combined with a viewport, the pair (viewport, surface) fully specifies where and how your app is mounted.
Two Axes: Viewport and Surface
Every entry in your manifest's ui_extension.views is defined by two orthogonal properties:
| Axis | Answers | Example |
|---|---|---|
| Viewport | Where is the user? | teachfloor.dashboard.course.detail |
| Surface | How is your app rendered? | drawer or widget |
An app can register multiple entries at the same viewport as long as they use different surfaces (or, for widgets, different widget ids — see Multiple Widgets). Kit picks the correct component using both fields.
Available Surfaces
Drawer
A right-side panel opened from the app dock. This is the default surface — if a manifest entry omits surface, it renders as a drawer for back-compat with pre-surfaces apps.
Typical use: on-demand focus interactions — open the app, look at something, close it. Cohort presence, quick notes, calculators, help articles.
Code
The surface field is optional here — omitting it produces the same result:
Code
Widget
A cell in a layout that hosts widget slots. Widgets are always visible on the page (no click-to-open) and sit alongside native platform widgets.
Typical use: always-on displays — dashboards, progress panels, live counters, feature highlights.
Widget entries carry an extra widget block with three required fields:
Code
id— stable slug (^[a-z][a-z0-9_]*$), unique per app across all widget declarations. Used as the runtime identifier when your bundle contains multiple widget components. Part of the composite key stored in the dashboard row, so renaming your component doesn't invalidate placed layouts.name— shown in the admin's widget picker and in the app install-consent surface list. ≤60 chars.description— one-line explanation shown alongside the name in the picker. ≤200 chars.
A Full Manifest Example
An app registering a drawer plus two widgets:
Code
The two widgets share the same viewport — Kit disambiguates by widget.id at render time.
Reading the Surface at Runtime
The current surface is exposed via useExtensionContext(), grouped inside environment:
Code
Prefer the SURFACES constant over the bare 'drawer' / 'widget' strings — it's exported specifically to survive future renames.
Apps that don't need surface-aware behavior can ignore environment.surface entirely — most components render the same regardless of surface.
Widget Auto-Height with <WidgetView>
Requires @teachfloor/extension-kit ≥ 1.27.0. Earlier versions of the kit don't ship <WidgetView> or the SURFACES constant. Bump your app's dependency before wrapping widgets in <WidgetView>.
Wrap your widget's root element in <WidgetView> and the slot in the dashboard grid will automatically fit your content's height. No aspect ratio needed:
Code
WidgetView accepts the same layout props as Container (p, px, py, sx, etc.) — spacing shorthands work directly.
Under the hood, WidgetView observes its own DOM height and emits it to the host over RPC. The host applies the reported height to the slot's container, so a widget with sparse content stays compact and a widget with a scrolling list grows to match. The same mechanism powers <SettingsView>.
Admin override: if the dashboard admin explicitly picks an aspect ratio when configuring the widget slot, that ratio wins over the emitted height. Auto-height is the default; forced ratios are the escape hatch.
Multiple Widgets at the Same Viewport
A single app bundle can register multiple widgets at the same viewport as long as each has a distinct widget.id. This lets one deploy provide, say, a Course Progress widget and a Learning Streak widget from the same codebase.
Kit picks each widget's component using environment.view.id:
Code
In most cases you can skip this dispatch entirely and let ExtensionViewLoader (from @teachfloor/extension-kit) resolve the right component by mapping widget.id to your components — see the Extension Kit Components chapter.
Runtime environment.viewport is always the actual route
A widget declared as viewport: '*' that lands on the dashboard receives environment.viewport === 'teachfloor.dashboard.dashboard.detail' at runtime — never '*'. Wildcards are declaration scope; the runtime value is always concrete. This lets your widget code react to where it landed (fetch course-specific data on a course page, org-wide data on the main dashboard) without conflating declaration with location.
CLI Helpers
The Teachfloor CLI scaffolds drawer views and widgets with the correct manifest shape:
Code
See the CLI reference for the full flag list.
Continue to
- Extension Kit Components — the UI primitives you'll use inside your surfaces, including
<WidgetView>and<SettingsView> - Extension Kit Integration — surface-agnostic APIs (events, storage, navigation) that work identically across drawer and widget