Styleframe Logo
Forms

Field Group

A layout component for joining buttons, inputs, selects, and other bordered controls into a single unit with merged borders and shared orientation. Supports horizontal and vertical layouts plus full-width block mode through the recipe system.

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:

src/components/field-group.styleframe.ts
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:

src/components/FieldGroup.tsx
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.

OrientationDirectionBorder Collapsing
horizontalLeft to right (row)Right border + right/left radius removed at joins
verticalTop 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.

Pro tip: The block variant works with both orientations. Combine 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.

Direct children only. The seam rules rely on each control being a direct child and on :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 with role="group" and an aria-label describing 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), consider role="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:

src/components/field-group.styleframe.ts
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:

src/components/field-group.styleframe.ts
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;
Good to know: Filtering also removes compound variants and adjusts default variants that reference filtered-out values, so your recipe stays consistent.

API Reference

useFieldGroupRecipe(s, options?)

Creates a field group recipe with orientation and block mode variants plus automatic border collapsing for direct children.

Parameters:

ParameterTypeDescription
sStyleframeThe Styleframe instance
optionsDeepPartial<RecipeConfig>Optional overrides for the recipe configuration
options.baseVariantDeclarationsBlockCustom base styles for the field group
options.variantsVariantsCustom variant definitions for the recipe
options.defaultVariantsRecord<keyof Variants, string>Default variant values for the recipe
options.compoundVariantsCompoundVariant[]Custom compound variant definitions for the recipe
options.filterRecord<string, string[]>Limit which variant values are generated

Variants:

VariantOptionsDefault
orientationhorizontal, verticalhorizontal
blocktrue, falsefalse

Learn more about recipes →

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 primary action button with neutral/outline controls so users can quickly identify the primary action.
  • Add an aria-label: Always describe the group's purpose with aria-label so screen readers can communicate the relationship between controls.

FAQ