Appearance
Flexbox布局 - 完整弹性盒子布局指南
1. Flexbox基础概念
1.1 什么是Flexbox
Flexbox(弹性盒子布局)是CSS3引入的一维布局模型,用于在容器中高效地排列、对齐和分配空间,即使元素大小未知或动态变化。
typescript
const flexboxConcepts = {
container: {
name: 'Flex容器',
property: 'display: flex | inline-flex',
role: '包含flex项目的父元素'
},
items: {
name: 'Flex项目',
role: 'Flex容器的直接子元素',
behavior: '按flex规则排列'
},
mainAxis: {
name: '主轴',
direction: 'row | row-reverse | column | column-reverse',
property: 'flex-direction'
},
crossAxis: {
name: '交叉轴',
direction: '垂直于主轴',
alignment: 'align-items, align-content'
}
};1.2 Flexbox术语
css
/* 主轴和交叉轴 */
.container {
display: flex;
/* 主轴方向 */
flex-direction: row; /* 默认,水平从左到右 */
flex-direction: row-reverse; /* 水平从右到左 */
flex-direction: column; /* 垂直从上到下 */
flex-direction: column-reverse; /* 垂直从下到上 */
}
/* 主轴: main axis
主轴起点: main start
主轴终点: main end
主轴尺寸: main size
交叉轴: cross axis
交叉轴起点: cross start
交叉轴终点: cross end
交叉轴尺寸: cross size */2. 容器属性
2.1 display
css
/* 块级flex容器 */
.flex-container {
display: flex;
width: 100%;
}
/* 行内flex容器 */
.inline-flex {
display: inline-flex;
/* 容器本身表现为行内元素 */
}2.2 flex-direction
css
/* 主轴方向 */
.row {
display: flex;
flex-direction: row; /* 默认 */
/* 项目水平排列,从左到右 */
}
.row-reverse {
display: flex;
flex-direction: row-reverse;
/* 项目水平排列,从右到左 */
}
.column {
display: flex;
flex-direction: column;
/* 项目垂直排列,从上到下 */
}
.column-reverse {
display: flex;
flex-direction: column-reverse;
/* 项目垂直排列,从下到上 */
}2.3 flex-wrap
css
/* 换行控制 */
.nowrap {
display: flex;
flex-wrap: nowrap; /* 默认,不换行 */
/* 项目会缩小以适应容器 */
}
.wrap {
display: flex;
flex-wrap: wrap;
/* 项目会换行,从上到下 */
}
.wrap-reverse {
display: flex;
flex-wrap: wrap-reverse;
/* 项目会换行,从下到上 */
}
/* flex-flow: flex-direction和flex-wrap的简写 */
.container {
display: flex;
flex-flow: row wrap;
/* 等同于:
flex-direction: row;
flex-wrap: wrap; */
}2.4 justify-content
css
/* 主轴对齐 */
.justify-start {
display: flex;
justify-content: flex-start; /* 默认,起点对齐 */
}
.justify-end {
display: flex;
justify-content: flex-end; /* 终点对齐 */
}
.justify-center {
display: flex;
justify-content: center; /* 居中对齐 */
}
.justify-between {
display: flex;
justify-content: space-between;
/* 两端对齐,项目之间间隔相等 */
}
.justify-around {
display: flex;
justify-content: space-around;
/* 每个项目两侧间隔相等 */
}
.justify-evenly {
display: flex;
justify-content: space-evenly;
/* 项目和容器边缘间隔都相等 */
}2.5 align-items
css
/* 交叉轴对齐(单行) */
.align-stretch {
display: flex;
align-items: stretch; /* 默认,拉伸填充容器 */
}
.align-start {
display: flex;
align-items: flex-start; /* 起点对齐 */
}
.align-end {
display: flex;
align-items: flex-end; /* 终点对齐 */
}
.align-center {
display: flex;
align-items: center; /* 居中对齐 */
}
.align-baseline {
display: flex;
align-items: baseline; /* 基线对齐 */
}2.6 align-content
css
/* 交叉轴对齐(多行) */
.content-stretch {
display: flex;
flex-wrap: wrap;
align-content: stretch; /* 默认,行拉伸填充 */
}
.content-start {
display: flex;
flex-wrap: wrap;
align-content: flex-start; /* 起点对齐 */
}
.content-end {
display: flex;
flex-wrap: wrap;
align-content: flex-end; /* 终点对齐 */
}
.content-center {
display: flex;
flex-wrap: wrap;
align-content: center; /* 居中对齐 */
}
.content-between {
display: flex;
flex-wrap: wrap;
align-content: space-between; /* 两端对齐 */
}
.content-around {
display: flex;
flex-wrap: wrap;
align-content: space-around; /* 周围对齐 */
}2.7 gap
css
/* 项目间隔 */
.gap {
display: flex;
gap: 1rem; /* 所有方向间隔 */
}
.row-gap {
display: flex;
flex-wrap: wrap;
row-gap: 1rem; /* 行间隔 */
column-gap: 2rem; /* 列间隔 */
}
/* 简写 */
.gap-both {
display: flex;
gap: 1rem 2rem; /* row-gap column-gap */
}3. 项目属性
3.1 order
css
/* 排序 */
.item {
order: 0; /* 默认值 */
}
.item-first {
order: -1; /* 排在前面 */
}
.item-last {
order: 1; /* 排在后面 */
}
/* 示例 */
.container {
display: flex;
}
.item:nth-child(1) { order: 3; }
.item:nth-child(2) { order: 1; }
.item:nth-child(3) { order: 2; }
/* 显示顺序: 2, 3, 1 */3.2 flex-grow
css
/* 放大比例 */
.item {
flex-grow: 0; /* 默认,不放大 */
}
.grow-1 {
flex-grow: 1; /* 占据剩余空间的1份 */
}
.grow-2 {
flex-grow: 2; /* 占据剩余空间的2份 */
}
/* 示例 */
.container {
display: flex;
width: 600px;
}
.item-a {
width: 100px;
flex-grow: 1; /* 剩余空间的1/3 */
}
.item-b {
width: 100px;
flex-grow: 2; /* 剩余空间的2/3 */
}
/* item-a最终宽度: 100 + (600-200) * 1/3 = 233px
item-b最终宽度: 100 + (600-200) * 2/3 = 367px */3.3 flex-shrink
css
/* 缩小比例 */
.item {
flex-shrink: 1; /* 默认,等比缩小 */
}
.no-shrink {
flex-shrink: 0; /* 不缩小 */
}
.shrink-more {
flex-shrink: 2; /* 缩小更多 */
}
/* 示例 */
.container {
display: flex;
width: 400px;
}
.item-a {
width: 300px;
flex-shrink: 1;
}
.item-b {
width: 300px;
flex-shrink: 2;
}
/* 超出: 300 + 300 - 400 = 200px
item-a缩小: 200 * (300*1)/(300*1 + 300*2) = 67px
item-b缩小: 200 * (300*2)/(300*1 + 300*2) = 133px
item-a最终: 300 - 67 = 233px
item-b最终: 300 - 133 = 167px */3.4 flex-basis
css
/* 主轴初始大小 */
.item {
flex-basis: auto; /* 默认,由内容决定 */
}
.fixed-basis {
flex-basis: 200px; /* 固定基准值 */
}
.percentage-basis {
flex-basis: 50%; /* 百分比 */
}
.content-basis {
flex-basis: content; /* 基于内容 */
}
/* 与width/height的关系 */
.item {
width: 100px;
flex-basis: 200px; /* flex-basis优先级更高 */
}3.5 flex简写
css
/* flex: flex-grow flex-shrink flex-basis */
.item {
flex: 0 1 auto; /* 默认值 */
}
.flex-1 {
flex: 1;
/* 等同于: flex: 1 1 0% */
/* 平分剩余空间 */
}
.flex-auto {
flex: auto;
/* 等同于: flex: 1 1 auto */
/* 基于内容大小,占据剩余空间 */
}
.flex-none {
flex: none;
/* 等同于: flex: 0 0 auto */
/* 不伸缩 */
}
.flex-initial {
flex: initial;
/* 等同于: flex: 0 1 auto */
/* 默认值 */
}
/* 常用值 */
.equal-width {
flex: 1; /* 等宽 */
}
.twice-width {
flex: 2; /* 两倍宽 */
}
.fixed-width {
flex: 0 0 200px; /* 固定200px */
}3.6 align-self
css
/* 单个项目的交叉轴对齐 */
.item {
align-self: auto; /* 默认,继承容器的align-items */
}
.self-start {
align-self: flex-start;
}
.self-end {
align-self: flex-end;
}
.self-center {
align-self: center;
}
.self-stretch {
align-self: stretch;
}
.self-baseline {
align-self: baseline;
}4. 常见布局模式
4.1 水平垂直居中
css
/* 方法1: justify + align */
.center-1 {
display: flex;
justify-content: center;
align-items: center;
}
/* 方法2: margin auto */
.center-2 {
display: flex;
}
.center-2 > * {
margin: auto;
}
/* 方法3: 项目设置 */
.center-3 {
display: flex;
}
.centered-item {
margin: auto;
}4.2 等宽布局
css
/* 等宽列 */
.equal-columns {
display: flex;
}
.equal-columns > * {
flex: 1;
/* 或 flex: 1 1 0% */
}
/* 带间隔的等宽 */
.equal-with-gap {
display: flex;
gap: 1rem;
}
.equal-with-gap > * {
flex: 1;
}4.3 固定+自适应
css
/* 固定侧边栏 + 自适应内容 */
.layout {
display: flex;
}
.sidebar {
flex: 0 0 250px; /* 固定250px */
}
.main {
flex: 1; /* 自适应剩余空间 */
}
/* 响应式 */
@media (max-width: 768px) {
.layout {
flex-direction: column;
}
.sidebar {
flex: 0 0 auto;
}
}4.4 圣杯布局
css
/* 页眉+内容+页脚 */
.holy-grail {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.header,
.footer {
flex: 0 0 auto; /* 固定高度 */
}
.content {
display: flex;
flex: 1; /* 占据剩余空间 */
}
.nav {
flex: 0 0 200px; /* 固定侧边栏 */
order: -1; /* 移到左侧 */
}
.main {
flex: 1; /* 主内容自适应 */
}
.aside {
flex: 0 0 200px; /* 固定侧边栏 */
}4.5 卡片网格
css
/* 自适应卡片网格 */
.card-grid {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.card {
flex: 1 1 300px; /* 最小300px,自动换行 */
max-width: 100%;
}
/* 或使用calc */
.card-fixed {
flex: 0 0 calc(33.333% - 1rem);
}
@media (max-width: 768px) {
.card-fixed {
flex: 0 0 calc(50% - 1rem);
}
}
@media (max-width: 480px) {
.card-fixed {
flex: 0 0 100%;
}
}5. React Flexbox组件
5.1 Flex容器组件
tsx
// Flex.tsx
interface FlexProps {
children: React.ReactNode;
direction?: 'row' | 'row-reverse' | 'column' | 'column-reverse';
wrap?: 'nowrap' | 'wrap' | 'wrap-reverse';
justify?: 'flex-start' | 'flex-end' | 'center' | 'space-between' | 'space-around' | 'space-evenly';
align?: 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline';
alignContent?: 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'space-between' | 'space-around';
gap?: string | number;
className?: string;
}
export function Flex({
children,
direction = 'row',
wrap = 'nowrap',
justify = 'flex-start',
align = 'stretch',
alignContent,
gap,
className = ''
}: FlexProps) {
const styles: React.CSSProperties = {
display: 'flex',
flexDirection: direction,
flexWrap: wrap,
justifyContent: justify,
alignItems: align,
alignContent,
gap
};
return (
<div style={styles} className={className}>
{children}
</div>
);
}
// 使用
<Flex direction="row" justify="space-between" align="center" gap="1rem">
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
</Flex>5.2 Flex项目组件
tsx
// FlexItem.tsx
interface FlexItemProps {
children: React.ReactNode;
grow?: number;
shrink?: number;
basis?: string | number;
order?: number;
alignSelf?: 'auto' | 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline';
className?: string;
}
export function FlexItem({
children,
grow = 0,
shrink = 1,
basis = 'auto',
order,
alignSelf,
className = ''
}: FlexItemProps) {
const styles: React.CSSProperties = {
flexGrow: grow,
flexShrink: shrink,
flexBasis: basis,
order,
alignSelf
};
return (
<div style={styles} className={className}>
{children}
</div>
);
}
// 使用
<Flex>
<FlexItem grow={1} basis="200px">Flexible</FlexItem>
<FlexItem grow={0} shrink={0} basis="300px">Fixed</FlexItem>
<FlexItem grow={2}>Double width</FlexItem>
</Flex>5.3 布局组件库
tsx
// Layout.tsx - 常用布局组件
// 水平垂直居中
export function Center({ children }: { children: React.ReactNode }) {
return (
<Flex justify="center" align="center" className="h-full">
{children}
</Flex>
);
}
// 页面布局
export function PageLayout({
header,
children,
footer
}: {
header: React.ReactNode;
children: React.ReactNode;
footer: React.ReactNode;
}) {
return (
<Flex direction="column" className="min-h-screen">
<header className="flex-none">{header}</header>
<main className="flex-1">{children}</main>
<footer className="flex-none">{footer}</footer>
</Flex>
);
}
// 侧边栏布局
export function SidebarLayout({
sidebar,
children
}: {
sidebar: React.ReactNode;
children: React.ReactNode;
}) {
return (
<Flex className="h-full">
<aside className="flex-none w-64">{sidebar}</aside>
<main className="flex-1 overflow-auto">{children}</main>
</Flex>
);
}
// 响应式卡片网格
export function CardGrid({ children }: { children: React.ReactNode }) {
return (
<Flex wrap="wrap" gap="1rem">
{Children.map(children, child => (
<div className="flex-grow-0 flex-shrink-0 basis-[300px] max-w-full">
{child}
</div>
))}
</Flex>
);
}
// Stack组件
export function Stack({
children,
spacing = '1rem',
direction = 'column'
}: {
children: React.ReactNode;
spacing?: string;
direction?: 'row' | 'column';
}) {
return (
<Flex direction={direction} gap={spacing}>
{children}
</Flex>
);
}6. Flexbox与其他布局对比
6.1 Flexbox vs Grid
css
/* Flexbox - 一维布局 */
.flex-layout {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.flex-layout > * {
flex: 1 1 300px;
}
/* Grid - 二维布局 */
.grid-layout {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1rem;
}
/* 何时使用Flexbox:
- 一维排列(行或列)
- 内容驱动的布局
- 动态数量的项目
- 简单的对齐需求 */
/* 何时使用Grid:
- 二维布局(行和列)
- 精确的布局控制
- 复杂的网格系统
- 固定的布局结构 */6.2 Flexbox vs Float
css
/* Float - 传统布局 */
.float-layout::after {
content: '';
display: table;
clear: both;
}
.float-layout > * {
float: left;
width: 33.333%;
}
/* Flexbox - 现代布局 */
.flex-layout {
display: flex;
}
.flex-layout > * {
flex: 1;
}
/* Flexbox优势:
- 不需要清除浮动
- 更好的垂直对齐
- 等高列
- 更灵活的排序
- 响应式更简单 */7. 实战案例
7.1 导航栏
tsx
// Navbar.tsx
export function Navbar() {
return (
<nav className="flex items-center justify-between p-4 bg-white shadow">
{/* Logo */}
<div className="flex-none">
<img src="/logo.svg" alt="Logo" className="h-8" />
</div>
{/* 导航链接 */}
<ul className="flex gap-6 flex-1 justify-center">
<li><a href="/">首页</a></li>
<li><a href="/products">产品</a></li>
<li><a href="/about">关于</a></li>
</ul>
{/* 操作按钮 */}
<div className="flex gap-3 flex-none">
<button className="btn-secondary">登录</button>
<button className="btn-primary">注册</button>
</div>
</nav>
);
}
// 响应式导航
export function ResponsiveNavbar() {
const [isOpen, setIsOpen] = useState(false);
const isMobile = useMediaQuery('(max-width: 768px)');
return (
<nav className="flex flex-wrap items-center justify-between p-4">
<div className="flex items-center justify-between w-full md:w-auto">
<img src="/logo.svg" alt="Logo" className="h-8" />
{isMobile && (
<button onClick={() => setIsOpen(!isOpen)}>
菜单
</button>
)}
</div>
<ul className={`
flex flex-col md:flex-row gap-4
w-full md:w-auto md:flex-1 md:justify-center
${isMobile && !isOpen ? 'hidden' : 'flex'}
`}>
<li><a href="/">首页</a></li>
<li><a href="/products">产品</a></li>
<li><a href="/about">关于</a></li>
</ul>
<div className="flex gap-3">
<button>登录</button>
<button>注册</button>
</div>
</nav>
);
}7.2 表单布局
tsx
// Form.tsx
export function FlexForm() {
return (
<form className="flex flex-col gap-6 max-w-2xl mx-auto p-6">
{/* 单行字段 */}
<div className="flex flex-col gap-2">
<label htmlFor="name">姓名</label>
<input type="text" id="name" className="input" />
</div>
{/* 并排字段 */}
<div className="flex gap-4">
<div className="flex-1 flex flex-col gap-2">
<label htmlFor="email">邮箱</label>
<input type="email" id="email" className="input" />
</div>
<div className="flex-1 flex flex-col gap-2">
<label htmlFor="phone">电话</label>
<input type="tel" id="phone" className="input" />
</div>
</div>
{/* 按钮组 */}
<div className="flex gap-3 justify-end">
<button type="button" className="btn-secondary">取消</button>
<button type="submit" className="btn-primary">提交</button>
</div>
</form>
);
}7.3 卡片列表
tsx
// CardList.tsx
export function CardList({ items }: { items: Item[] }) {
return (
<div className="flex flex-wrap gap-6">
{items.map(item => (
<div
key={item.id}
className="flex-grow-0 flex-shrink-0 basis-[calc(33.333%-1rem)] max-w-full"
>
<Card item={item} />
</div>
))}
</div>
);
}
// Card组件
function Card({ item }: { item: Item }) {
return (
<div className="flex flex-col h-full border rounded-lg overflow-hidden">
<img src={item.image} alt={item.title} className="flex-none h-48 object-cover" />
<div className="flex-1 flex flex-col p-4">
<h3 className="text-xl font-bold mb-2">{item.title}</h3>
<p className="flex-1 text-gray-600 mb-4">{item.description}</p>
<div className="flex justify-between items-center">
<span className="text-2xl font-bold">${item.price}</span>
<button className="btn-primary">加入购物车</button>
</div>
</div>
</div>
);
}8. 性能优化
8.1 避免布局抖动
typescript
// 避免频繁的flex计算
// 使用固定值或缓存计算结果
// ❌ 不好 - 每次渲染都计算
function BadComponent() {
return (
<div style={{ flex: calculateFlex() }}>
Content
</div>
);
}
// ✅ 好 - 缓存计算结果
function GoodComponent() {
const flexValue = useMemo(() => calculateFlex(), []);
return (
<div style={{ flex: flexValue }}>
Content
</div>
);
}8.2 使用will-change
css
/* 优化flex动画 */
.animated-flex {
will-change: flex-basis;
transition: flex-basis 0.3s;
}
.animated-flex:hover {
flex-basis: 300px;
}9. 最佳实践
typescript
const flexboxBestPractices = {
container: [
'明确设置display: flex',
'合理使用flex-wrap处理溢出',
'使用gap代替margin',
'注意主轴和交叉轴方向',
'为多行布局设置align-content'
],
items: [
'优先使用flex简写',
'理解flex-grow的计算方式',
'设置flex-shrink防止意外缩小',
'使用flex-basis代替width/height',
'谨慎使用order改变顺序'
],
responsive: [
'移动端优先使用column',
'桌面端切换到row',
'使用媒体查询调整flex值',
'考虑换行策略',
'测试不同内容长度'
],
accessibility: [
'保持逻辑DOM顺序',
'谨慎使用order',
'确保键盘导航顺序',
'屏幕阅读器兼容',
'足够的触摸目标'
]
};10. 总结
Flexbox布局的核心要点:
- 容器属性: display, flex-direction, flex-wrap, justify-content, align-items
- 项目属性: flex-grow, flex-shrink, flex-basis, order, align-self
- 简写属性: flex, flex-flow
- 间隔: gap替代margin
- 响应式: 配合媒体查询
- 性能: 避免频繁计算
- 可访问性: 保持DOM顺序
通过掌握Flexbox,可以轻松实现各种复杂的响应式布局。