属性传递 #

一、attrs 基础 #

1.1 什么是 attrs #

attrs 方法允许你在样式组件上预设置 HTML 属性:

jsx
import styled from 'styled-components';

const Input = styled.input.attrs({
  type: 'text',
  placeholder: 'Enter text...',
})`
  padding: 12px 16px;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  font-size: 16px;
`;

function App() {
  return <Input />;
}

渲染结果:

html
<input type="text" placeholder="Enter text..." class="sc-xxx" />

1.2 attrs 与 styled 链式调用 #

jsx
const Input = styled.input
  .attrs({ type: 'text' })`
  padding: 12px;
  border: 2px solid #e0e0e0;
`;

二、静态属性 #

2.1 设置固定属性 #

jsx
const EmailInput = styled.input.attrs({
  type: 'email',
  autoComplete: 'email',
})`
  padding: 12px 16px;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
`;

const PasswordInput = styled.input.attrs({
  type: 'password',
  autoComplete: 'current-password',
})`
  padding: 12px 16px;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
`;

const NumberInput = styled.input.attrs({
  type: 'number',
  min: 0,
  max: 100,
  step: 1,
})`
  padding: 12px 16px;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  width: 100px;
`;

2.2 设置 ARIA 属性 #

jsx
const Button = styled.button.attrs({
  role: 'button',
  'aria-label': 'Click to submit',
})`
  padding: 12px 24px;
  background: #667eea;
  color: white;
  border-radius: 8px;
`;

const IconButton = styled.button.attrs({
  'aria-hidden': 'true',
})`
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: #f0f0f0;
  border: none;
  cursor: pointer;
`;

2.3 设置 data 属性 #

jsx
const Card = styled.div.attrs({
  'data-testid': 'card',
})`
  padding: 24px;
  background: white;
  border-radius: 12px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
`;

const ListItem = styled.li.attrs({
  'data-role': 'list-item',
})`
  padding: 12px;
  border-bottom: 1px solid #eee;
`;

三、动态属性 #

3.1 基于 Props 的动态属性 #

jsx
const Input = styled.input.attrs(props => ({
  type: props.type || 'text',
  placeholder: props.placeholder || 'Enter value',
  disabled: props.disabled,
}))`
  padding: 12px 16px;
  border: 2px solid ${props => props.disabled ? '#ccc' : '#e0e0e0'};
  border-radius: 8px;
  background: ${props => props.disabled ? '#f5f5f5' : 'white'};
`;

function App() {
  return (
    <>
      <Input placeholder="Name" />
      <Input type="email" placeholder="Email" />
      <Input disabled placeholder="Disabled" />
    </>
  );
}

3.2 计算属性 #

jsx
const Input = styled.input.attrs(props => ({
  type: props.type || 'text',
  maxLength: props.maxLength || 100,
  minLength: props.minLength || 0,
}))`
  padding: 12px 16px;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  width: ${props => props.fullWidth ? '100%' : 'auto'};
`;

const RangeInput = styled.input.attrs(props => ({
  type: 'range',
  min: props.min || 0,
  max: props.max || 100,
  step: props.step || 1,
  defaultValue: props.defaultValue || 50,
}))`
  width: 100%;
  height: 8px;
  background: #e0e0e0;
  border-radius: 4px;
  outline: none;
`;

3.3 条件属性 #

jsx
const Button = styled.button.attrs(props => ({
  disabled: props.loading || props.disabled,
  'aria-busy': props.loading,
  type: props.submit ? 'submit' : 'button',
}))`
  padding: 12px 24px;
  background: ${props => props.loading ? '#ccc' : '#667eea'};
  color: white;
  border-radius: 8px;
  cursor: ${props => props.loading ? 'wait' : 'pointer'};
  opacity: ${props => props.disabled ? 0.6 : 1};
`;

function App() {
  return (
    <>
      <Button>Normal Button</Button>
      <Button loading>Loading...</Button>
      <Button submit>Submit Form</Button>
      <Button disabled>Disabled</Button>
    </>
  );
}

四、属性与样式联动 #

4.1 属性值用于样式 #

jsx
const Input = styled.input.attrs(props => ({
  size: props.$size || 20,
}))`
  padding: 12px;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  width: ${props => props.size}ch;
`;

const Progress = styled.progress.attrs(props => ({
  max: props.max || 100,
  value: props.value || 0,
}))`
  width: 100%;
  height: 8px;
  border-radius: 4px;
  
  &::-webkit-progress-bar {
    background: #e0e0e0;
    border-radius: 4px;
  }
  
  &::-webkit-progress-value {
    background: #667eea;
    border-radius: 4px;
  }
`;

4.2 属性默认值 #

jsx
const Input = styled.input.attrs(props => ({
  type: props.type || 'text',
  placeholder: props.placeholder || `Enter ${props.name || 'value'}`,
  required: props.required ?? true,
}))`
  padding: 12px 16px;
  border: 2px solid ${props => props.required ? '#ffccc7' : '#e0e0e0'};
  border-radius: 8px;
`;

function App() {
  return (
    <form>
      <Input name="username" placeholder="Username" />
      <Input name="email" type="email" />
      <Input name="bio" required={false} />
    </form>
  );
}

五、类型安全 #

5.1 TypeScript 类型定义 #

tsx
import styled from 'styled-components';

interface InputProps {
  $size?: 'small' | 'medium' | 'large';
  type?: string;
  placeholder?: string;
}

const Input = styled.input.attrs<InputProps>(props => ({
  type: props.type || 'text',
  placeholder: props.placeholder || 'Enter value',
}))`
  padding: ${props => 
    props.$size === 'small' ? '8px 12px' :
    props.$size === 'large' ? '16px 24px' :
    '12px 16px'
  };
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  font-size: ${props => 
    props.$size === 'small' ? '14px' :
    props.$size === 'large' ? '18px' :
    '16px'
  };
`;

5.2 扩展 HTML 属性 #

tsx
import styled from 'styled-components';

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  $variant?: 'primary' | 'secondary' | 'danger';
  $loading?: boolean;
}

const Button = styled.button.attrs<ButtonProps>(props => ({
  disabled: props.$loading || props.disabled,
  type: props.type || 'button',
}))`
  padding: 12px 24px;
  border-radius: 8px;
  font-size: 16px;
  cursor: ${props => props.$loading ? 'wait' : 'pointer'};
  
  ${props => props.$variant === 'primary' && `
    background: #667eea;
    color: white;
    border: none;
  `}
  
  ${props => props.$variant === 'secondary' && `
    background: transparent;
    color: #667eea;
    border: 2px solid #667eea;
  `}
  
  ${props => props.$variant === 'danger' && `
    background: #ff4d4f;
    color: white;
    border: none;
  `}
  
  ${props => props.$loading && `
    opacity: 0.7;
  `}
`;

六、高级用法 #

6.1 多次链式调用 #

jsx
const Input = styled.input
  .attrs({ type: 'text' })
  .attrs(props => ({
    placeholder: props.placeholder || 'Enter text',
  }))
  .attrs(props => ({
    disabled: props.disabled,
  }))`
  padding: 12px;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
`;

6.2 属性合并 #

jsx
const BaseInput = styled.input.attrs({
  autoComplete: 'off',
  spellCheck: 'false',
})`
  padding: 12px;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
`;

const EmailInput = styled(BaseInput).attrs({
  type: 'email',
  autoComplete: 'email',
})`
  background: #f0f7ff;
`;

const PasswordInput = styled(BaseInput).attrs({
  type: 'password',
  autoComplete: 'current-password',
})`
  background: #fff5f5;
`;

6.3 属性工厂函数 #

jsx
const createInput = (type, defaultProps = {}) => styled.input.attrs({
  type,
  ...defaultProps,
})`
  padding: 12px 16px;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  font-size: 16px;
`;

const TextInput = createInput('text', {
  placeholder: 'Enter text',
});

const EmailInput = createInput('email', {
  placeholder: 'Enter email',
  autoComplete: 'email',
});

const NumberInput = createInput('number', {
  min: 0,
  max: 100,
});

七、实际应用场景 #

7.1 表单组件 #

jsx
import styled from 'styled-components';

const FormInput = styled.input.attrs(props => ({
  type: props.type || 'text',
  required: props.required,
  'aria-label': props['aria-label'] || props.name,
}))`
  width: 100%;
  padding: 12px 16px;
  border: 2px solid ${props => props.error ? '#ff4d4f' : '#e0e0e0'};
  border-radius: 8px;
  font-size: 16px;
  
  &:focus {
    border-color: #667eea;
    outline: none;
    box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
  }
  
  &::placeholder {
    color: #aaa;
  }
`;

const FormTextarea = styled.textarea.attrs(props => ({
  rows: props.rows || 4,
  'aria-label': props['aria-label'] || props.name,
}))`
  width: 100%;
  padding: 12px 16px;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  font-size: 16px;
  resize: vertical;
  min-height: 100px;
  
  &:focus {
    border-color: #667eea;
    outline: none;
  }
`;

const FormSelect = styled.select.attrs(props => ({
  'aria-label': props['aria-label'] || props.name,
}))`
  width: 100%;
  padding: 12px 16px;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  font-size: 16px;
  background: white;
  cursor: pointer;
  
  &:focus {
    border-color: #667eea;
    outline: none;
  }
`;

7.2 可访问性组件 #

jsx
const AccessibleButton = styled.button.attrs(props => ({
  role: 'button',
  'aria-label': props['aria-label'] || props.title,
  'aria-disabled': props.disabled,
  tabIndex: props.disabled ? -1 : 0,
}))`
  padding: 12px 24px;
  background: #667eea;
  color: white;
  border: none;
  border-radius: 8px;
  cursor: ${props => props.disabled ? 'not-allowed' : 'pointer'};
  opacity: ${props => props.disabled ? 0.6 : 1};
  
  &:focus-visible {
    outline: 2px solid #667eea;
    outline-offset: 2px;
  }
  
  &:hover:not(:disabled) {
    background: #5a6fd6;
  }
`;

const IconButton = styled.button.attrs(props => ({
  'aria-label': props.label,
  title: props.label,
}))`
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: #f0f0f0;
  border: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  
  &:hover {
    background: #e0e0e0;
  }
  
  &:focus-visible {
    outline: 2px solid #667eea;
    outline-offset: 2px;
  }
`;

7.3 图像组件 #

jsx
const Image = styled.img.attrs(props => ({
  loading: props.loading || 'lazy',
  decoding: 'async',
  alt: props.alt || '',
}))`
  max-width: 100%;
  height: auto;
  border-radius: ${props => props.rounded ? '50%' : '8px'};
  object-fit: ${props => props.cover ? 'cover' : 'contain'};
`;

const Avatar = styled.img.attrs(props => ({
  loading: 'lazy',
  alt: props.alt || 'User avatar',
}))`
  width: ${props => props.size || '48px'};
  height: ${props => props.size || '48px'};
  border-radius: 50%;
  object-fit: cover;
  border: 2px solid #e0e0e0;
`;

八、attrs vs Props #

8.1 区别对比 #

jsx
const InputWithAttrs = styled.input.attrs({
  type: 'text',
})`
  padding: 12px;
`;

const InputWithProps = styled.input`
  padding: 12px;
`;

function App() {
  return (
    <>
      <InputWithAttrs />
      <InputWithProps type="text" />
    </>
  );
}

8.2 选择建议 #

text
使用 attrs 的场景
├── 固定的 HTML 属性(type, disabled, readOnly)
├── 默认属性值
├── 可访问性属性(aria-*, role)
├── 表单属性(name, form, autoComplete)
└── 数据属性(data-*)

使用 Props 的场景
├── 动态样式值
├── 条件样式
├── 组件状态
└── 主题相关

九、性能考虑 #

9.1 避免内联函数 #

jsx
const Input = styled.input.attrs(props => ({
  type: props.type || 'text',
  placeholder: props.placeholder || 'Enter value',
}))`
  padding: 12px;
`;

const getInputAttrs = (props) => ({
  type: props.type || 'text',
  placeholder: props.placeholder || 'Enter value',
});

const OptimizedInput = styled.input.attrs(getInputAttrs)`
  padding: 12px;
`;

9.2 静态属性优化 #

jsx
const staticAttrs = {
  type: 'text',
  autoComplete: 'off',
  spellCheck: 'false',
};

const Input = styled.input.attrs(staticAttrs)`
  padding: 12px;
  border: 2px solid #e0e0e0;
`;

十、总结 #

attrs 使用技巧速查表:

用法 示例
静态属性 .attrs({ type: 'text' })
动态属性 .attrs(p => ({ disabled: p.loading }))
链式调用 .attrs({}).attrs(p => ({}))
属性合并 继承时自动合并
类型安全 .attrs<Props>(p => ({}))

下一步:学习 动画支持 掌握 CSS 动画和关键帧的使用。

最后更新:2026-03-28