Primitives
Primitives
The building blocks every component uses: surfaces, icons, the state layer and the focus ring.
- COMPONENT_SPEC §39
- CSS · custom elements · React
- Open in the playground
Examples
Surfaces
Surface containers, tonal elevation levels and shapes, from tokens.
import { Surface } from '@kyralabs/m3exp-web/react';
const TILE = {
display: 'grid',
placeItems: 'center',
inlineSize: 'calc(var(--m3e-sys-space-900) + var(--m3e-sys-space-800))',
blockSize: 'var(--m3e-sys-space-900)',
};
export default function Surfaces() {
return (
<>
<Surface container="lowest" shape="medium" style={TILE}>
Lowest
</Surface>
<Surface container="high" shape="medium" elevation={1} style={TILE}>
High, level 1
</Surface>
<Surface container="highest" shape="large" elevation={3} style={TILE}>
Highest, level 3
</Surface>
<Surface variant shape="extra-large" style={TILE}>
Variant
</Surface>
</>
);
}
<div
class="m3e-surface m3e-surface--container-lowest m3e-surface--shape-medium"
style="
display: grid;
place-items: center;
inline-size: calc(var(--m3e-sys-space-900) + var(--m3e-sys-space-800));
block-size: var(--m3e-sys-space-900);
"
>
Lowest
</div>
<div
class="m3e-surface m3e-surface--container-high m3e-surface--shape-medium m3e-surface--elevation-1"
style="
display: grid;
place-items: center;
inline-size: calc(var(--m3e-sys-space-900) + var(--m3e-sys-space-800));
block-size: var(--m3e-sys-space-900);
"
>
High, level 1
</div>
<div
class="m3e-surface m3e-surface--container-highest m3e-surface--shape-large m3e-surface--elevation-3"
style="
display: grid;
place-items: center;
inline-size: calc(var(--m3e-sys-space-900) + var(--m3e-sys-space-800));
block-size: var(--m3e-sys-space-900);
"
>
Highest, level 3
</div>
<div
class="m3e-surface m3e-surface--variant m3e-surface--shape-extra-large"
style="
display: grid;
place-items: center;
inline-size: calc(var(--m3e-sys-space-900) + var(--m3e-sys-space-800));
block-size: var(--m3e-sys-space-900);
"
>
Variant
</div>
Run the HTML version (CSS classes only).
Icons
Material Symbols, outlined or filled (FILL 1). Icons are decorative unless they have a label. Weight, grade and optical size are axes too: see the fonts and icons guide.
The live demo runs with JavaScript. Its code is below.
import { Icon } from '@kyralabs/m3exp-web/react';
export default function Icons() {
return (
<>
<Icon name="favorite" />
<Icon name="favorite" fill />
<Icon name="home" />
<Icon name="home" fill />
<Icon name="info" label="Information" />
</>
);
}
<m3e-icon name="favorite"></m3e-icon>
<m3e-icon name="favorite" fill></m3e-icon>
<span class="m3e-icon" aria-hidden="true">home</span>
<span class="m3e-icon" aria-hidden="true" data-fill>home</span>
<m3e-icon name="info" label="Information"></m3e-icon>
Run the HTML version (custom elements, no framework).
API
React
<Surface>
Surface primitive (COMPONENT_SPEC §39): the .m3e-surface classes as a <div>.
| Prop | Type | Default | Description |
|---|---|---|---|
container
|
"default" | "lowest" | "low" | "high" | "highest"
|
Tonal surface container (surface-container-* roles). Omit for surface.
|
|
variant
|
boolean
|
surface-variant / on-surface-variant instead of surface / on-surface.
|
|
elevation
|
0 | 1 | 2 | 3 | 4 | 5
|
Elevation level 0–5 (two-layer shadow tokens). | |
shape
|
"none" | "small" | "medium" | "large" | "extra-large" | "extra-small" | "full"
|
Corner shape token. |
Also accepts HTML attributes not listed here.
<Icon>
Material Symbols icon (D5, COMPONENT_SPEC §39). Renders the m3e-css .m3e-icon contract, the same DOM as <m3e-icon>.
| Prop | Type | Default | Description |
|---|---|---|---|
name
required
|
string
|
Material Symbols ligature, e.g. home.
|
|
fill
|
boolean
|
FILL axis on (the filled style; m3.material.io uses it for selected icons). | |
weight
|
number
|
wght axis (100–700).
|
|
grade
|
number
|
GRAD axis (−50–200).
|
|
opticalSize
|
number
|
opsz axis (20–48); match it to the rendered size.
|
|
label
|
string
|
Accessible name. Without it the icon is decorative (aria-hidden).
|
Also accepts HTML attributes not listed here.
useStateLayer()
Drives the m3e-css state-layer ripple (data-ripple, data-pressed, --_m3e-ripple-*), the same attributes m3e-elements' attachRipple writes. Hover and focus overlays need no JS.
Pass hostRef when the overlay lives on a container while a child takes the presses (interactive cards): the ripple then starts from the press point within the host.
Custom elements
<m3e-icon>
import '@kyralabs/m3e-elements/icon.js';
<m3e-icon name="home" fill weight="500" grade="-25" optical-size="20" label="Home"> (D5, COMPONENT_SPEC §39). Renders the m3e-css contract inside itself (the host is display: contents):
``html <span class="m3e-icon" aria-hidden="true" data-fill style="--m3e-comp-icon-weight: 500">home</span> ``
Decorative by default (aria-hidden); label makes it role="img" with that name. Without a name, the element's initial text content is used.
| Attribute | Property | Type | Notes |
|---|---|---|---|
name
|
name
|
string
|
|
fill
|
fill
|
boolean
|
|
weight
|
weight
|
number
|
|
grade
|
grade
|
number
|
|
optical-size
|
opticalSize
|
number
|
|
label
|
label
|
string
|
<m3e-navigation-item>
import '@kyralabs/m3e-elements/wide-navigation-rail.js';
<m3e-navigation-item value="home" icon="home" href="/">Home</m3e-navigation-item> — data for one destination of a <m3e-short-navigation-bar>, <m3e-wide-navigation-rail> or <m3e-modal-wide-navigation-rail> (it renders nothing itself; the container renders the .m3e-navigation-item contract): value (default: the label), icon (Material Symbols, filled while selected), href (renders a link; otherwise a button that only selects), disabled, label-hidden (icon only; the label becomes the accessible name), badge (a Compose Badge on the icon, as NavigationSuiteScaffold's NavigationItemIcon builds it: "" the dot, digits a count — 999+ above 999 — other text as is) and badge-label (the badge's visually hidden text, read after the label: default "New notification" for the dot, the shown text otherwise; "" leaves it out). The label is the text content. In a <m3e-navigation-drawer> (Compose NavigationDrawerItem) the icon is optional and badge is the trailing badge text instead (badge-label unused).
| Attribute | Property | Type | Notes |
|---|---|---|---|
value
|
value
|
string
|
|
icon
|
icon
|
string
|
|
href
|
href
|
string
|
|
disabled
|
disabled
|
boolean
|
|
label-hidden
|
labelHidden
|
boolean
|
|
badge
|
badge
|
string
|
|
badge-label
|
badgeLabel
|
string
|
CSS
surface.css
@import '@kyralabs/m3e-css/surface.css';
Surface primitive (COMPONENT_SPEC §39, FOUNDATIONS §5). Tonal surface containers instead of surface tint; elevation as the two-layer shadow tokens (--m3e-sys-elevation-levelN-shadow).
<div class="m3e-surface m3e-surface--container-high m3e-surface--elevation-1
m3e-surface--shape-medium">…</div>
1 custom properties it reads
| Custom property | Default |
|---|---|
--m3e-comp-divider-thickness
|
1px |
icon.css
@import '@kyralabs/m3e-css/icon.css';
Icon primitive (D5, COMPONENT_SPEC §39): a Material Symbols ligature.
<span class="m3e-icon" aria-hidden="true">home</span>
<span class="m3e-icon" data-fill aria-hidden="true">home</span> (FILL 1, selected)
Family: --m3e-ref-typeface-icon, set by @kyralabs/m3e-tokens/material-symbols*.css or by the CSS that m3e-icons subset writes. Axes default to the font's fvar defaults (--m3e-ref-typeface-icon-*) and are overridden per icon with --m3e-comp-icon-fill, -weight, -grade and -optical-size. Size: --m3e-comp-icon-size, else Compose's default Icon size (Icon.kt@19991f2c L219: SmallIconButtonTokens.IconSize).
6 custom properties it reads
| Custom property | Default |
|---|---|
--m3e-comp-icon-fill
|
set by the component |
--m3e-comp-icon-grade
|
set by the component |
--m3e-comp-icon-optical-size
|
set by the component |
--m3e-comp-icon-size
|
set by the component |
--m3e-comp-icon-weight
|
set by the component |
--m3e-comp-small-icon-button-icon-size
|
24px |
state-layer.css
@import '@kyralabs/m3e-css/state-layer.css';
State layer primitive (FOUNDATIONS §6, COMPONENT_SPEC §39). The one place hover, focus, pressed and dragged overlays are drawn: components never re-implement it. A component opts in by adding its root class to the :where() lists below; anything else can use .m3e-state-layer.
- Overlay: ::before in the host's own stacking context (isolation), under the content.
- Color: --m3e-comp-state-layer-color (components set their content role), else currentColor.
- Opacity: --m3e-sys-state-* (StateTokens.kt), animated with the default effects spring.
Hover, focus and dragged write --_m3e-state-layer-opacity (the resting layer); a press
overrides the opacity.
- Pressed: :active, or [data-pressed] set by m3e-elements / m3e-react. With [data-ripple] a
press grows a circle from --_m3e-ripple-x/-y to --_m3e-ripple-radius (REQUIREMENTS §12
ripple approximation) while the resting layer stays visible around it, as Compose draws the
ripple over a state layer that ignores presses (material-ripple Ripple.kt@19991f2c
StateLayer, L433–503). Inside the circle the two layers composite
(rest + pressed − rest × pressed). Release fades back to the resting layer; the circle does
not shrink.
- Disabled hosts (:disabled, [aria-disabled='true']) draw no state layer.
1 custom properties it reads
| Custom property | Default |
|---|---|
--m3e-comp-state-layer-color
|
set by the component |
focus-ring.css
@import '@kyralabs/m3e-css/focus-ring.css';
Focus ring primitive (FOUNDATIONS §6, COMPONENT_SPEC §0 "Focus"). Keyboard focus only (:focus-visible), never on pointer clicks. A component opts in by adding its root class to the :where() lists; anything else can use .m3e-focus-ring (.m3e-focus-ring--inward draws inside).
Thickness and offsets: md.sys.state.focus-indicator (m3.material.io, matches Material Web). Color: secondary (every androidx *Tokens.FocusIndicatorColor). The ring is an outline, so it follows the host's shape, as Compose's focusRingShape (material3 Ripple.kt@19991f2c L85–89).
Motion (owner decision D19 (3), SOURCES register #61): Compose's inset ring. Its width and inset scale with a 0 → 1 interpolation (material3-ripple Ripple.kt@19991f2c L406–418) that grows on the fast spatial spring and shrinks on the fast effects spring (material3 Ripple.kt L852–853; material3-ripple Ripple.kt L327–340). Here the outline's width and offset do the same: at rest every ring element carries a zero-width outline, and the width and offset transition on those springs' linear() curves. None under reduced motion. Components that set their own transition list the ring's first: var(--_m3e-focus-ring-transition, outline-width, outline-offset) (the fallback, without this file, transitions nothing).
navigation-item.css
@import '@kyralabs/m3e-css/navigation-item.css';
Navigation item: the destination shared by the short navigation bar, the wide navigation rail and the legacy (D4) navigation bar / rail (COMPONENT_SPEC §13, §14). The container sets the geometry, colors and motion through --_m3e-nav-* properties; the item is never used alone.
<a class="m3e-navigation-item" href="/" aria-current="page">
<span class="m3e-navigation-item__content">
<span class="m3e-navigation-item__indicator" aria-hidden="true"></span>
<span class="m3e-navigation-item__state" aria-hidden="true"></span>
<span class="m3e-icon m3e-navigation-item__icon" aria-hidden="true">home</span>
<span class="m3e-navigation-item__label">Home</span>
</span>
</a>
With a badge (badge.css; Compose BadgedBox in the item's icon slot), the label comes first and the icon follows in a badged box, so the badge's hidden text is read after the label ("Inbox 8"; owner decision M9 #7). Without a label the item's aria-label carries both and the badge has no hidden label:
<span class="m3e-navigation-item__label">Inbox</span>
<span class="m3e-badged-box">
<span class="m3e-icon m3e-navigation-item__icon" aria-hidden="true">inbox</span>
<span class="m3e-badge m3e-badge--large">
<span class="m3e-badge__text" aria-hidden="true">8</span>
<span class="m3e-badge__label">8</span>
</span>
</span>
Selected: aria-current (any value but "false") or data-selected. Disabled: aria-disabled="true". Layout: icon above the label; icon before the label inside a horizontal short navigation bar (Compose NavigationItemIconPosition.Start). The wide navigation rail lays its items out itself (wide-navigation-rail.css: one formula of the animated icon position). The badged box takes the icon's place in every layout. Badges are drawn as Compose places them: over the label in the horizontal bar (placeLabelAndStartIcon places the icon last), under it in the wide rail (placeAnimatedLabelAndIcon places the label last).
Badge bounds (Compose Modifier.badgeBounds() on NavigationItemLayout and AnimatedNavigationItemLayout, NavigationItem.kt L571 / L624; legacy NavigationBar.kt L536, NavigationRail.kt L542): the layout fills the item, so a badge never leaves the item. Each container sets --_m3e-badged-box-bounds-end / -top (badge.css) to the icon's distances from the item's end and top edges.
1 custom properties it reads
| Custom property | Default |
|---|---|
--m3e-comp-minimum-interactive-component-size
|
48px |
Sources
Values come from androidx Compose Material3 1.5.0-beta01 at 19991f2c and from
m3.material.io, as cited below from COMPONENT_SPEC §39 and the m3e-css sources.
navigation-item.css sources
(compose/material3 @ 19991f2c):
- NavigationItem.kt (ShortNavigationBarItem, WideNavigationRailItem): the whole item is the target, but the ripple, hover/focus overlay and focus ring sit on the indicator pill (IndicatorRipple,
ripple(focusRingShape = indicatorShape), MappedInteractionSource). The ripple color is the container's content color (LocalContentColor = onSurface). - The indicator grows from the center: width = full width × progress, alpha = progress, both from one animateFloatAsState (animateIndicatorProgressAsState: DefaultSpatial). The legacy NavigationBarItem / NavigationRailItem animate width on FastSpatial and alpha on DefaultEffects.
- Top-icon layout: indicator pill above the label (TopIconOrIconOnlyMeasurePolicy); without a label the pill alone is centered. Start-icon layout: the pill hugs icon + label (StartIconMeasurePolicy, AnimatedMeasurePolicy).
- Icon and label colors: NavigationItemColors (selected / unselected / disabled = unselected at DisabledAlpha). Selected icons switch to FILL 1 (COMPONENT_SPEC §0, §13).