Field Group
Overview
The Field Group is a layout component that visually connects related controls — buttons, inputs, selects, badges — into a single, cohesive unit. The useFieldGroupRecipe() composable creates a fully configured recipe with orientation and block mode options, plus automatic border-radius collapsing so adjacent controls share a seamless edge.
Children are placed as direct children of the group. The recipe targets them generically, so any bordered control works without per-component wiring — an Input next to a Button, a Select prefixing an Input, or a row of buttons. It generates type-safe utility classes at build time with zero runtime CSS.
Why use the Field Group recipe?
The Field Group recipe helps you:
- Join controls seamlessly: Adjacent children automatically collapse their border-radius and inner border at shared edges, creating a clean, unified control.
- Compose any controls: Buttons, inputs, and selects join the same way — no slot wrappers or per-type variants. Fields stretch to take the slack in a horizontal group while buttons stay intrinsic.
- Support both orientations: Switch between horizontal and vertical layouts with a single variant prop — border collapsing adjusts automatically.
- Fill available space: The block variant stretches the group to full width, perfect for search bars, subscribe forms, and mobile layouts.
Usage
Register the recipe
Add the Field Group recipe to a local Styleframe instance. The global styleframe.config.ts provides design tokens and utilities, while the component-level file registers the recipe itself:
import { styleframe } from 'virtual:styleframe';
import {
useButtonRecipe,
useInputRecipe,
useFieldGroupRecipe,
} from '@styleframe/theme';
const s = styleframe();
const button = useButtonRecipe(s);
const input = useInputRecipe(s);
const fieldGroup = useFieldGroupRecipe(s);
export default s;
Build the component
Import the fieldGroup runtime function from the virtual module and pass variant props to compute class names. Render the controls you want to join as direct children:
import { fieldGroup, type FieldGroupProps } from "virtual:styleframe";
export function FieldGroup({
orientation = "horizontal",
block = false,
children,
}: FieldGroupProps & { children?: React.ReactNode }) {
const classes = fieldGroup({ orientation, block: block ? "true" : "false" });
return (
<div className={classes} role="group">
{children}
</div>
);
}
See it in action
Orientation
The Field Group supports two orientation variants that control the layout direction and which edges have their borders collapsed.
Horizontal
The default orientation. Controls are laid out side-by-side in a row. Border-radius is removed from the right edge of each child except the last, and from the left edge of each child except the first. The right border is removed from each child except the last to prevent doubled borders. Inputs and selects flex-grow to fill the remaining space while buttons keep their intrinsic width.
Vertical
Controls are stacked top-to-bottom in a column. Border-radius is removed from the bottom edge of each child except the last, and from the top edge of each child except the first. The bottom border is removed from each child except the last to prevent doubled borders.
| Orientation | Direction | Border Collapsing |
|---|---|---|
horizontal | Left to right (row) | Right border + right/left radius removed at joins |
vertical | Top to bottom (column) | Bottom border + bottom/top radius removed at joins |
Block
The block variant stretches the field group to fill the full width of its container. Inputs and selects already absorb the free space in a horizontal group; buttons are given equal flex sizing (flex-basis: 0; flex-grow: 1) so a row of buttons distributes evenly.
block with vertical for a full-width stacked layout, such as a compose box with an action button beneath it.Composing controls
A field group joins whatever bordered controls you nest inside it. Pair a Select with an Input for a currency field, or wrap a Button around an Input for a search bar.
:first-child / :last-child position. Render conditional children with v-if (or equivalent) so they are removed from the DOM rather than hidden with display: none, which would shift the first/last edges. Overlay panels (e.g. a Select's dropdown) must be nested inside their own control, never placed as a direct group child.Accessibility
- Use
role="group". Wrap the controls in an element withrole="group"and anaria-labeldescribing the group's purpose so screen readers announce the context.
<!-- Correct: semantic group with label -->
<div role="group" aria-label="Search the site">
<input class="input-field" />
<button class="...">Search</button>
</div>
- Keyboard navigation. Native
<button>,<input>, and<select>elements provide Tab key support out of the box. For single-selection button groups (segmented controls), considerrole="toolbar"with arrow-key navigation.
Customization
Overriding Defaults
The useFieldGroupRecipe() composable accepts an optional second argument to override any part of the recipe configuration. Overrides are deep-merged with the defaults, so you only need to specify the properties you want to change:
import { styleframe } from 'virtual:styleframe';
import { useFieldGroupRecipe } from '@styleframe/theme';
const s = styleframe();
const fieldGroup = useFieldGroupRecipe(s, {
defaultVariants: {
orientation: 'vertical',
block: 'true',
},
});
export default s;
Filtering Variants
If you only need a subset of the available variants, use the filter option to limit which values are generated. This reduces the output CSS and keeps your component API focused:
import { styleframe } from 'virtual:styleframe';
import { useFieldGroupRecipe } from '@styleframe/theme';
const s = styleframe();
// Only generate horizontal orientation
const fieldGroup = useFieldGroupRecipe(s, {
filter: {
orientation: ['horizontal'],
},
});
export default s;
API Reference
useFieldGroupRecipe(s, options?)
Creates a field group recipe with orientation and block mode variants plus automatic border collapsing for direct children.
Parameters:
| Parameter | Type | Description |
|---|---|---|
s | Styleframe | The Styleframe instance |
options | DeepPartial<RecipeConfig> | Optional overrides for the recipe configuration |
options.base | VariantDeclarationsBlock | Custom base styles for the field group |
options.variants | Variants | Custom variant definitions for the recipe |
options.defaultVariants | Record<keyof Variants, string> | Default variant values for the recipe |
options.compoundVariants | CompoundVariant[] | Custom compound variant definitions for the recipe |
options.filter | Record<string, string[]> | Limit which variant values are generated |
Variants:
| Variant | Options | Default |
|---|---|---|
orientation | horizontal, vertical | horizontal |
block | true, false | false |
Best Practices
- Render controls as direct children: The border-collapsing selectors target the group's direct children by position. Don't wrap individual controls in extra elements, and don't hide children with
display: none— remove them from the DOM instead. - Keep a single size: All controls in a group should use the same size for consistent alignment along the seam.
- Use block mode for full-width forms: Search bars and subscribe forms read best when the group fills its container and the input absorbs the free space.
- Maintain a visual hierarchy: Combine a
primaryaction button withneutral/outlinecontrols so users can quickly identify the primary action. - Add an aria-label: Always describe the group's purpose with
aria-labelso screen readers can communicate the relationship between controls.
FAQ
flex-grow: 1 so they absorb the free space — the natural behaviour for a [input][Search] search bar. Buttons and badges stay at their intrinsic width. If you want a button to stretch too, use the block variant, which equal-fills buttons.display: inline-flex and only takes up as much space as its children need. In block mode, the group switches to display: flex with width: 100%; inputs absorb the free space and buttons are given equal flex sizing.useInputGroupRecipe, useInputPrependRecipe, and useInputAppendRecipe. The Field Group replaces all of them with one explicit, composable wrapper: drop your controls in as direct children and the seams merge automatically. Inline prefix / suffix addons that render inside a field are unchanged.Color Picker
A color selection control composed of a saturation/value selector square, a vertical hue track, and shared draggable thumbs. Renders any color through a CSS variable and gradients, with five sizes through the recipe system.
Input
A text-field component with a wrapper-owned visual surface, inline prefix/suffix addons, and invalid/disabled/readonly states. Supports light, dark, and neutral colors, default/soft/ghost styles, and three sizes through the recipe system.