Inline
Horizontal layout primitive. Wraps by default, because a row of buttons that overflows on a phone is the most common responsive bug in a design system.
Playground
Change a prop and watch it. The code below follows along.
Live
Open full cardInstall and import
Inline is in @dovetail-ds/react. Install it with React, if you haven't:
npm install @dovetail-ds/react react react-dom
import { Inline } from "@dovetail-ds/react";
Import @dovetail-ds/react/styles.css once at your app's root, then your theme after it. Setup covers the stylesheets, fonts and dark mode.
Guidelines
Horizontal layout primitive. Wraps by default, because a row of buttons that overflows on a phone is the most common responsive bug in a design system.
Use it when
- Laying out buttons, chips, avatars, icons with labels, toolbar items.
Don't use it when
- You need a fixed column structure. Use
Grid. - It is a run of text with inline links. Use ordinary flow.
Example
<Inline gap="xs" justify="flex-end">
<Button variant="secondary">Cancel</Button>
<Button>Save changes</Button>
</Inline>
Variants
Set wrap={false} only when overflow is handled another way, such as a scrolling toolbar.
Layers and spacing
layer sets the gap by how closely the neighbours belong together (related, group, block or section) instead of by step, and the layout's character moves it: spacing="tight" for a technical toolbar, open for room to breathe. It wins over gap. Where each layer sits in both directions is on Foundations, Layout.
<Inline layer="related">
<Icon name="clock" />
<Text>4 days</Text>
</Inline>
Tokens
--dt-space-inline-* for gap, and --dt-layout-inline-* for layer.
Props
The typed contract, from Inline.d.ts.
import * as React from "react";
/** Horizontal layout. Wraps by default so button rows survive narrow viewports. */
export interface InlineProps extends React.HTMLAttributes<HTMLElement> {
/** Gap from the inline axis of the space scale. @default "sm" */
gap?: "2xs" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
/** How closely the things either side of this gap belong together, from the layout layers: `related` (parts of one thing), `group` (members of a set), `block` (one unit from the next), `section` (a theme from the next). Sets the gap from `--dt-layout-inline-*`, which the layout's character moves as one, and wins over `gap`. */
layer?: "related" | "group" | "block" | "section";
/** The layout's character for this element and everything inside it: `tight` (technical), `balanced` or `open` (breathing room). Sets `data-layout`, which re-declares the layer tokens here. Inherited from the page when not set. */
spacing?: "tight" | "balanced" | "open";
/** @default "center" */
align?: React.CSSProperties["alignItems"];
justify?: React.CSSProperties["justifyContent"];
/** Allow children to wrap onto a new row. @default true */
wrap?: boolean;
/** @default "div" */
as?: keyof React.JSX.IntrinsicElements;
children?: React.ReactNode;
}
export declare function Inline(props: InlineProps): React.JSX.Element;
Tokens it reads
Every custom property Inline.jsx resolves, read from the source: 12 semantic. Component tier first, because that is the one to reach for.
- semantic Shared roles. Override one and everything using that role moves with it.
| Token | Tier | Declared as |
|---|---|---|
--dt-layout-inline-block | semantic | var(--dt-dim-6) |
--dt-layout-inline-group | semantic | var(--dt-dim-3) |
--dt-layout-inline-related | semantic | var(--dt-dim-2) |
--dt-layout-inline-section | semantic | var(--dt-dim-12) |
--dt-layout-scale | semantic | 1 |
--dt-space-inline-2xl | semantic | var(--dt-dim-12) |
--dt-space-inline-2xs | semantic | var(--dt-dim-1) |
--dt-space-inline-lg | semantic | var(--dt-dim-6) |
--dt-space-inline-md | semantic | var(--dt-dim-4) |
--dt-space-inline-sm | semantic | var(--dt-dim-3) |
--dt-space-inline-xl | semantic | var(--dt-dim-8) |
--dt-space-inline-xs | semantic | var(--dt-dim-2) |
Source
React only, no dependencies, every value a token.
import React from "react";
const GAPS = { "2xs": "var(--dt-space-inline-2xs)", xs: "var(--dt-space-inline-xs)", sm: "var(--dt-space-inline-sm)", md: "var(--dt-space-inline-md)", lg: "var(--dt-space-inline-lg)", xl: "var(--dt-space-inline-xl)", "2xl": "var(--dt-space-inline-2xl)" };
/* Drawn at --dt-layout-scale, 1 on a page, so a surface at another size keeps
its proportions. */
const scaled = (token) => `calc(var(${token}) * var(--dt-layout-scale, 1))`;
const LAYERS = { related: scaled("--dt-layout-inline-related"), group: scaled("--dt-layout-inline-group"), block: scaled("--dt-layout-inline-block"), section: scaled("--dt-layout-inline-section") };
export function Inline({ gap = "sm", layer, spacing, align = "center", justify, wrap = true, as: Tag = "div", children, style, ...rest }) {
return (
<Tag data-layout={spacing} style={{ display: "flex", flexDirection: "row", gap: LAYERS[layer] || GAPS[gap] || GAPS.sm, alignItems: align, justifyContent: justify, flexWrap: wrap ? "wrap" : "nowrap", ...style }} {...rest}>
{children}
</Tag>
);
}
In context
Inline also appears on the primitives reference cards, alongside the rest of its family.