Skip to content

Responsive local styles

Read as Markdown

Use style.when for ordered patches based on available containing size, in Pibbl logical units. Bounds are inclusive: minWidth, maxWidth, minHeight, and maxHeight. Conditions within one rule all need to match.

There are two authoring boundaries:

  • Outer placement style: a layout parent inspects its child’s style against the parent’s content size before assigning that child a box.
  • Inner content style: a primitive returned by a placed plain component sees the box assigned to that component. Put rules here to adapt inside a narrow cell.

A Flow placed directly in a 1000-wide Grid queries 1000, even if its eventual cell is 320 wide. A plain component placed in that cell can return a Flow whose rules query 320. Extracting a primitive into such a component changes the query basis; keep this distinction explicit during refactoring.

import {
Flow,
Grid,
Rectangle,
signal,
type BoxStyle,
type LayoutItemStyle,
type PibblResponsiveStyle,
type FlowStyle,
type SignalValue,
} from "@pibbl/core";
const gap = signal(12);
const contentStyle = {
width: "100%",
height: 112,
direction: "vertical",
gap,
when: [
{
query: { minWidth: 600 },
style: { direction: "horizontal", height: 44 },
},
{ query: { maxHeight: 120 }, style: { gap: 8 } },
],
} satisfies PibblResponsiveStyle<FlowStyle>;
type ActionsProps = {
style?: SignalValue<
PibblResponsiveStyle<BoxStyle & LayoutItemStyle> | undefined
>;
};
function Actions({ style: placementStyle }: ActionsProps) {
// The parent consumes this style for placement. Do not apply it a second time.
void placementStyle;
return (
<Flow style={contentStyle}>
<Rectangle style={{ width: 112, height: 44, fill: "#17324d" }} />
<Rectangle style={{ width: 112, height: 44, fill: "#8dd8ff" }} />
</Flow>
);
}
const narrowPanel = (
<Grid
style={{
width: 1000,
height: 160,
gridTemplateColumns: [320, "1fr"],
gridTemplateRows: [160],
}}
>
<Actions
style={{
width: "100%",
height: 160,
gridColumnStart: 1,
gridRowStart: 1,
}}
/>
</Grid>
);

The plain Actions function receives its original raw style prop; Pibbl neither resolves it for the function nor forwards it to descendants. Its parent inspects that prop for placement. Keep it named style so the parent can find it.

Render <Actions /> directly in a wide root to get a row, or in a 360-wide responsive root to get a column. The same contentStyle works in all three contexts. No root-size hook, observer, or additional signal is required.

The equivalent component without JSX uses the same values:

import { createElement, Flow, Rectangle } from "@pibbl/core";
function Actions({ style: placementStyle }: ActionsProps) {
void placementStyle;
return createElement(
Flow,
{ style: contentStyle },
createElement(Rectangle, {
style: { width: 112, height: 44, fill: "#17324d" },
}),
createElement(Rectangle, {
style: { width: 112, height: 44, fill: "#8dd8ff" },
}),
);
}

These are alternative declarations, not two functions to paste into one file. Try the store analytics example: select 30 days, then switch between Dashboard, Sidebar, and Phone. Its revenue card retains the selected period while its metrics and chart reflow. A short preview uses ordinary host scrolling; responsive styles do not add scrolling to Pibbl.

The base style applies first, then every matching patch in source order. Later assignments win. Objects and arrays replace whole values, including filters; there is no deep merge or cascade. Explicit undefined replaces an earlier value and then uses the primitive’s normal default or validation.

Fields in the base and every matching patch can be signals. All those fields are read even if overwritten later. Unmatched patch signals are not read or observed. A whole-style signal may replace the rules. The rule list, conditions, and patch objects themselves cannot be signals. Required base fields remain required even if a patch would supply them. custom stays opaque, and nested when is invalid.

style.minHeight constrains the receiver; query.minHeight tests its containing height. A patch that changes the receiver’s size never changes its own query basis. Its descendants can adapt to the resulting smaller or larger content box.

Give responsive layout items definite sizes or a definite allocated box. If a parent needs intrinsic (auto) measurement of a query-bearing item, Pibbl reports PIBBL_STYLE_QUERY_MEASUREMENT. Repair it by assigning a definite box and putting responsive content inside the allocated component, as above. Query-free Text children can still be measured inside an allocated responsive Flow.

Standalone measureElement supports a top-level query-bearing primitive only with finite maximum constraints on every queried axis. A queried descendant inspected during measurement remains unsupported. No previous-frame size or iterative layout solver is used.

Rules use the existing scheduler and receiver-owned signals. Static scenes stay idle; updates do not introduce another animation loop, observer, or layout pass. Selection cost grows with rule count and matching patch fields. Keep static lists outside render, as in this example, or use useConst for mount-local lists: recreating nested lists can invalidate retained Layer children. Framework timing nonregression remains a verification requirement, not a promise that unlimited rules cost nothing.

Intent Use
Adapt controls inside an assigned cell Inner responsive content style
Change an item’s placement within its parent Outer responsive placement style
Fit a short panel independently of width maxHeight or minHeight
Change children, labels, data density, or handlers Ordinary component code with useLayoutBox()
Detect pointer precision, hover, or reduced motion Not part of these size queries

Transforms, backing density, and CSS resizing of a fixed logical root do not change the query basis. Responsive roots follow their CSS content box. Layer children query their Layer’s logical allocation. Capability information is not needed for deterministic tests; narrow width does not imply touch input.

Responsive styles alone do not solve mobile usability. Touch targets, gestures, readability, data density, keyboard behavior, and accessible alternatives still need application design and testing. Opacity zero does not remove hit geometry.

Read the Drawing, layout, and effects companion for ownership, adaptation, failure modes, and verification. Agent start provides the version-selection workflow.

Documentation built with @pibbl/core 0.0.2, revision 2dccb19. ALPHA — NOT FOR PRODUCTION USE.