Flex
A low-level flexbox primitive that exposes the full flex API as props — four directions plus a responsive { base, md } form, three wrap modes, align-items incl. baseline, justify-content, align-content, a token gap scale with separate row and column gaps, inline mode, polymorphic render, and LTR/RTL. FlexItem sets grow, shrink, basis, order and alignSelf on one child, and fill gives it an equal share plus the min-size reset truncating text needs.
Playground
Installation
pnpm add @tessinaui/uiUsage
import { Flex, FlexItem } from "@tessinaui/ui";{/* A list row: pinned avatar, truncating text, trailing meta */}
<Flex align="center" gap="sm">
<FlexItem shrink={false}>
<Avatar name="Maya Chen" />
</FlexItem>
<FlexItem fill>
<p className="truncate">Maya Chen</p>
<p className="truncate text-muted-foreground">Q3 roadmap review — notes from Thursday</p>
</FlexItem>
<time className="shrink-0">9:41</time>
</Flex>
{/* Stacked on phones, a right-aligned row from 768px up */}
<Flex direction={{ base: "column-reverse", md: "row" }} justify="end" gap="sm">
<Button variant="secondary">Cancel</Button>
<Button>Save</Button>
</Flex>
{/* Wrapping with independent gaps */}
<Flex wrap="wrap" columnGap="sm" rowGap="md">
{tags.map((t) => <Chip key={t}>{t}</Chip>)}
</Flex>
{/* Semantic element */}
<Flex render={<nav aria-label="Primary" />} gap="sm">
<a href="/">Home</a>
<a href="/about">About</a>
</Flex>Examples
Default
A simple row of items with a medium gap, vertically centered.
Direction
row lays items out horizontally; column stacks them vertically.
Responsive direction
{ base, md } changes direction at the md band. This dialog footer stacks full-width below 768px — column-reverse puts the primary action on top — and becomes a right-aligned row from there up.
Align
baseline lines up mixed type sizes on their text baseline — a metadata row, or a price beside its unit. inline flows a Flex inside a sentence, and a column with align="center" builds a profile lockup.
Justify
Distribute items along the main axis with between, center, and evenly.
Wrap
Wrapping rows with independent columnGap and rowGap.
Gaps and line packing
The seven-step gap scale, then a wrapping tag cloud with a larger row gap than column gap. In a tray taller than its content, content="start" keeps the lines packed at the top instead of spreading them apart.
FlexItem grow
A fixed sidebar beside a grow main region that fills the remaining space.
FlexItem basis
Fractional basis presets build a three-column 1/4 · 2/4 · 1/4 layout.
FlexItem order and alignSelf
order="first" moves an image above its title visually while the DOM keeps the title first; alignSelf="end" drops one child to the bottom of a top-aligned row.
Media rows
The most common Flex shape in real products: a pinned avatar, a fill text block that truncates, and trailing meta. The lists render as <ul>/<li> through render, the settings rows are links with a 44px floor, and the last list shows the same rows under dir="rtl".
Toolbar
A search that grows, filter and sort controls, and a primary action pushed to the end with Spacer. The search has a basis, so on a phone it takes its own line instead of squeezing the buttons. Below it, a bulk-selection bar.
Header
Three zones in a semantic <header>: a pinned brand, a search that fills the middle, and pinned actions.
Stat row
Equal tiles from fill, stacked below 768px. The profile trio shows each value above its label with column-reverse, so screen readers still hear the label first.
Split pane
A pinned sidebar beside a fill content pane that scrolls inside a fixed height on desktop and stacks on phones.
Direction
Unlike Stack, Flex exposes all four flex-direction values directly.
| Value | CSS mapping |
|---|---|
"row" (default) | flex-row |
"column" | flex-col |
"row-reverse" | flex-row-reverse |
"column-reverse" | flex-col-reverse |
Responsive direction
Pass an object to change direction at the md band (768px). Both keys are required.
<Flex direction={{ base: "column", md: "row" }}>…</Flex>Flex emits two non-overlapping classes — max-md:flex-col md:flex-row — and no plain direction class beside them.
Don't make direction responsive with className. The default direction is emitted as a plain flex-row class, and a plain class beside a max-md: or md: variant of the same property is decided by stylesheet order, which Tailwind does not guarantee — className="max-md:flex-col" can silently lose. The object form is the supported way. The same applies to wrap, align and justify, which also emit plain defaults.
The bands follow the viewport, like Grid's cols and AspectRatio's ratio: a Flex in a narrow column on a wide screen keeps its md direction. Container-query bands are not offered — a container query resolves against an ancestor, so Flex cannot query its own width — and md is the only switch point today.
Wrap
| Value | CSS mapping |
|---|---|
"nowrap" (default) | flex-nowrap |
"wrap" | flex-wrap |
"wrap-reverse" | flex-wrap-reverse |
Gap
Seven presets that map to Tailwind's spacing scale.
| Value | Tailwind | px |
|---|---|---|
"none" | gap-0 | 0 |
"xs" | gap-1 | 4 |
"sm" | gap-2 | 8 |
"md" | gap-4 | 16 |
"lg" | gap-6 | 24 |
"xl" | gap-8 | 32 |
"2xl" | gap-12 | 48 |
Flex has no default gap — children sit flush until you set one. That is deliberate: Flex is the raw primitive, and Stack is the sibling that defaults to md.
For independent axis control use rowGap and columnGap — they accept the same seven values and override gap on their axis.
<Flex wrap="wrap" columnGap="xl" rowGap="sm">…</Flex>Align (align-items)
Cross-axis alignment of children.
| Value | CSS mapping |
|---|---|
"start" | items-start |
"center" | items-center |
"end" | items-end |
"stretch" (default) | items-stretch |
"baseline" | items-baseline |
Justify (justify-content)
Main-axis distribution. start and end are logical, so they follow dir.
| Value | CSS mapping |
|---|---|
"start" (default) | justify-start |
"center" | justify-center |
"end" | justify-end |
"between" | justify-between |
"around" | justify-around |
"evenly" | justify-evenly |
Content (align-content)
Applies only when wrap is enabled and the lines do not fill the cross axis. With no value, the lines stretch apart to fill a container taller than its content — set content="start" to keep them packed.
| Value | CSS mapping |
|---|---|
"start" | content-start |
"center" | content-center |
"end" | content-end |
"between" | content-between |
"around" | content-around |
"evenly" | content-evenly |
"stretch" | content-stretch |
"baseline" | content-baseline |
Inline
inline swaps flex for inline-flex so the container flows with surrounding text. Inside a <p>, render a <span> — a <div> is not allowed there.
<p>
Status <Flex inline render={<span />} align="center" gap="xs">…</Flex> since Tuesday.
</p>Polymorphic render
Preserve Flex styling while mounting semantic HTML.
<Flex direction="row" gap="md" render={<nav aria-label="Primary" />}>
<a href="/">Home</a>
<a href="/about">About</a>
</Flex>
<Flex render={<section />} direction="column" gap="lg">
<h2>Settings</h2>
<p>Adjust your preferences below.</p>
</Flex>Props already on the element win over the same props passed to Flex, and the element keeps its own ref and dir. The element's own children are kept when it has any; otherwise Flex's children fill it.
FlexItem
A helper for setting flex-child properties (flex-grow, flex-shrink, flex-basis, order, align-self) without reaching for Tailwind classes directly. FlexItem is optional — you can always apply grow, shrink-0, etc. inline on any child. It takes render too, including another Flex, so one element can be both an item and a container.
<Flex direction="row" gap="md">
<aside className="w-48">Sidebar</aside>
<FlexItem grow>
<main>Main (grows to fill)</main>
</FlexItem>
</Flex>fill
fill gives the item an equal share of the space and resets its minimum size.
| Classes | Why |
|---|---|
grow basis-[0%] | fill items split the space equally |
min-w-0 min-h-0 | A flex item defaults to min-width: auto, so without this a truncate label pushes its row wider instead of truncating, and a nested scroll area never scrolls |
- An explicit
groworbasisstill wins. - A zero-basis item never wraps.
wrapdecides line breaks from each item's basis, andfill's is 0. For tiles that should wrap, give them abasis(e.g.className="basis-48") andgrow. - It is written with longhands rather than
flex-1, so an explicitbasiscan never race it.
grow
| Value | CSS |
|---|---|
true / 1 | grow |
false / 0 | grow-0 |
2, 3, 4, 5 | flex-grow: n |
Values outside 0–5 emit no class.
shrink
| Value | CSS |
|---|---|
true / 1 | shrink |
false / 0 | shrink-0 |
2, 3, 4, 5 | flex-shrink: n |
Values outside 0–5 emit no class. Pin avatars, icons and sidebars with shrink={false}.
basis
Presets cover fractional layouts without arbitrary values.
"auto" · "full" · "0" · "1/2" · "1/3" · "2/3" · "1/4" · "2/4" · "3/4" · "1/5" · "2/5" · "3/5" · "4/5" · "1/6" · "5/6" · "1/12" through "11/12"
order
| Value | CSS |
|---|---|
"none" | order-none |
"first" | order-first |
"last" | order-last |
1 – 12 | order-n |
alignSelf
"auto" · "start" · "center" · "end" · "stretch" · "baseline" → self-*
RTL
dir="rtl" is forwarded to the DOM node and flips the main axis of row directions; justify start/end follow it. Column directions are unaffected by text direction. A render element keeps its own dir when Flex is given none.
Accessibility
- Flex is a structural primitive — it applies no ARIA roles.
- Use
renderto mount semantic HTML (<nav>,<ul>,<header>,<dl>) when the grouping has meaning. order,row-reverseandcolumn-reversemove pixels, not focus: keyboard and screen-reader order follow the DOM. Reversing a two-button footer on phones is a common, acceptable use — the primary action sits on top while the DOM keeps Cancel first — but never reorder content whose meaning depends on its sequence.- A pane that scrolls must be reachable by keyboard: give it focusable content (links, buttons), or
tabIndex={0}and an accessible name. inlinedoes not remove any semantics — the element remains a<div>(or whateverrenderprovides).- Both components stamp
data-slot(flex,flex-item) for stable targeting in tests and automation.
When to use Flex vs. Stack
Both are flex wrappers. Pick based on how much of the flex API you actually need.
| Use Stack when… | Use Flex when… |
|---|---|
| You want a single-axis layout with sensible defaults | You need the full flexbox API |
A default md gap suits you | You want no gap until you set one |
You want a divider between children | You need independent rowGap and columnGap |
| Children size themselves naturally | You need grow / shrink / basis / order / fill on children |
For many apps Stack is the right pick. Reach for Flex when you're building a layout that can't be expressed through Stack's opinionated API.
Usage rules
- Do give a growing text child
fill, so it can truncate instead of pushing its row wider. - Do pin avatars, icons and sidebars with
shrink={false}. - Do use
direction={{ base, md }}to change direction responsively. - Don't make
direction,wrap,alignorjustifyresponsive withmax-md:/md:classes — their defaults are plain classes and the pair races. - Don't hide a Flex with
max-md:hidden— itsflexdisplay class is plain too. Render it conditionally or wrap it. - Don't expect
fillitems to wrap; give wrapping tiles abasis. - Don't use Flex for a toolbar that needs arrow-key navigation — that is Toolbar.
API Reference
Flex Props
| Prop | Type | Default | Description |
|---|---|---|---|
direction | "row" | "column" | "row-reverse" | "column-reverse" | { base, md } | "row" | flex-direction; the object form changes it at the md band |
wrap | "nowrap" | "wrap" | "wrap-reverse" | "nowrap" | flex-wrap |
gap | "none" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | — | Shorthand for both row and column gap |
rowGap | Same as gap | — | Vertical gap (overrides gap on that axis) |
columnGap | Same as gap | — | Horizontal gap (overrides gap on that axis) |
align | "start" | "center" | "end" | "stretch" | "baseline" | "stretch" | align-items |
justify | "start" | "center" | "end" | "between" | "around" | "evenly" | "start" | justify-content |
content | "start" | "center" | "end" | "between" | "around" | "evenly" | "stretch" | "baseline" | — | align-content — only relevant with wrap |
inline | boolean | false | Uses inline-flex instead of flex |
render | ReactElement | — | Polymorphic target — clone this element and apply Flex styles |
dir | "ltr" | "rtl" | — | Text direction forwarded to the DOM node |
className | string | — | Extra classes on the root element |
FlexItem Props
| Prop | Type | Default | Description |
|---|---|---|---|
fill | boolean | — | Equal share of the space plus min-w-0 min-h-0 |
grow | boolean | 0 | 1 | 2 | 3 | 4 | 5 | — | flex-grow |
shrink | boolean | 0 | 1 | 2 | 3 | 4 | 5 | — | flex-shrink |
basis | Fraction preset (see table above) | — | flex-basis |
order | "none" | "first" | "last" | 1..12 | — | order — visual only |
alignSelf | "auto" | "start" | "center" | "end" | "stretch" | "baseline" | — | align-self |
render | ReactElement | — | Polymorphic target |
dir | "ltr" | "rtl" | — | Text direction forwarded to the DOM node |
className | string | — | Extra classes on the root element |
Notes
- No intrinsic padding. Flex only controls layout — add padding, borders, background via
className. - No SSR caveats. Flex renders the same markup on the server and the client.
contentis a prop, not an HTML attribute. React's nativecontentHTML attribute (meta-tag-only) is omitted from Flex's props so the CVA variant takes precedence.- Arbitrary values. If a fraction you need isn't in the
basispreset list, drop toclassName="basis-[...]"— FlexItem's class output is merged after its props. data-slot.flexon Flex,flex-itemon FlexItem, on both the plain and therenderpath.