Styling Components
Remix provides a powerfulcss prop for styling components with support for pseudo-selectors, pseudo-elements, nested rules, and media queries.
Basic CSS Prop
Use thecss prop to apply inline styles:
function Button() {
return () => (
<button
css={{
color: 'white',
backgroundColor: 'blue',
padding: '12px 24px',
borderRadius: '4px',
border: 'none',
cursor: 'pointer',
}}
>
Click me
</button>
)
}
CSS Prop vs Style Prop
Thecss prop creates static CSS rules, while style applies styles directly to the element.
Use css prop for:
- Static styles that don’t change
- Pseudo-selectors (
:hover,:focus, etc.) - Media queries
style prop for:
- Dynamic styles that change based on state or props
- Computed values that update frequently
// Good: Static in css, dynamic in style
function ProgressBar(handle: Handle) {
let progress = 0
return () => (
<div
css={{
backgroundColor: 'blue', // Static
height: '20px',
borderRadius: '4px',
}}
style={{
width: `${progress}%`, // Dynamic
}}
>
{progress}%
</div>
)
}
// Bad: Dynamic values in css prop
function ProgressBar(handle: Handle) {
let progress = 0
return () => (
<div
css={{
width: `${progress}%`, // Creates new CSS rule on every update
backgroundColor: 'blue',
}}
>
{progress}%
</div>
)
}
Pseudo-Selectors
Use& to reference the current element:
function Button() {
return () => (
<button
css={{
color: 'white',
backgroundColor: 'blue',
padding: '12px 24px',
borderRadius: '4px',
border: 'none',
cursor: 'pointer',
'&:hover': {
backgroundColor: 'darkblue',
transform: 'translateY(-1px)',
},
'&:active': {
backgroundColor: 'navy',
transform: 'translateY(0)',
},
'&:focus': {
outline: '2px solid yellow',
outlineOffset: '2px',
},
'&:disabled': {
opacity: 0.5,
cursor: 'not-allowed',
},
}}
>
Click me
</button>
)
}
Pseudo-Elements
Use&::before and &::after:
function Badge() {
return (props: { count: number }) => (
<div
css={{
position: 'relative',
display: 'inline-block',
'&::before': {
content: '""',
position: 'absolute',
top: '-4px',
right: '-4px',
width: '8px',
height: '8px',
backgroundColor: 'red',
borderRadius: '50%',
},
}}
>
{props.count > 0 && <span>{props.count}</span>}
</div>
)
}
Attribute Selectors
Use&[attribute] for attribute selectors:
function Input() {
return (props: { required?: boolean }) => (
<input
required={props.required}
css={{
padding: '8px',
border: '1px solid #ccc',
borderRadius: '4px',
'&[required]': {
borderColor: 'red',
},
'&[aria-invalid="true"]': {
borderColor: 'red',
outline: '2px solid red',
},
}}
/>
)
}
Descendant Selectors
Style child elements from the parent:import type { RemixNode } from 'remix/component'
function Card() {
return (props: { children: RemixNode }) => (
<div
css={{
padding: '20px',
border: '1px solid #ddd',
borderRadius: '8px',
backgroundColor: 'white',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
// Style descendants
'& h2': {
marginTop: 0,
fontSize: '24px',
fontWeight: 'bold',
},
'& p': {
color: '#666',
lineHeight: 1.6,
},
'& .icon': {
width: '24px',
height: '24px',
marginRight: '8px',
},
'& button': {
marginTop: '16px',
},
}}
>
{props.children}
</div>
)
}
Nested Selectors for Parent State
Use nested selectors when parent state affects children. This is preferable to managing state in JavaScript:// Good: CSS handles hover state
function Card() {
return (props: { children: RemixNode }) => (
<div
css={{
border: '1px solid #ddd',
borderRadius: '8px',
padding: '20px',
'&:hover': {
borderColor: 'blue',
// Parent hover affects children
'& .title': {
color: 'blue',
},
'& .description': {
opacity: 1,
},
},
'& .title': {
fontSize: '20px',
fontWeight: 'bold',
color: '#333',
},
'& .description': {
opacity: 0.7,
marginTop: '8px',
},
}}
>
<div className="title">Title</div>
<div className="description">Description</div>
</div>
)
}
// Bad: Managing hover state in JavaScript
function Card(handle: Handle) {
let isHovered = false
return (props: { children: RemixNode }) => (
<div
mix={[
on('mouseenter', () => {
isHovered = true
handle.update()
}),
on('mouseleave', () => {
isHovered = false
handle.update()
}),
]}
css={{
border: `1px solid ${isHovered ? 'blue' : '#ddd'}`,
}}
>
<div className="title" css={{ color: isHovered ? 'blue' : '#333' }}>
Title
</div>
</div>
)
}
Media Queries
Use@media for responsive design:
function ResponsiveGrid() {
return (props: { children: RemixNode }) => (
<div
css={{
display: 'grid',
gap: '16px',
gridTemplateColumns: '1fr',
'@media (min-width: 768px)': {
gridTemplateColumns: 'repeat(2, 1fr)',
},
'@media (min-width: 1024px)': {
gridTemplateColumns: 'repeat(3, 1fr)',
},
}}
>
{props.children}
</div>
)
}
Comprehensive Example
A product card demonstrating all features:function ProductCard() {
return (props: { title: string; price: number; image: string }) => (
<div
css={{
border: '1px solid #ddd',
borderRadius: '8px',
overflow: 'hidden',
transition: 'transform 0.2s, box-shadow 0.2s',
'&:hover': {
transform: 'translateY(-4px)',
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
'& .title': {
color: 'blue',
},
'& button': {
backgroundColor: 'darkblue',
},
},
'@media (max-width: 768px)': {
'&:hover': {
transform: 'translateY(-2px)',
},
},
}}
>
<img
src={props.image}
alt={props.title}
css={{
width: '100%',
height: '200px',
objectFit: 'cover',
'@media (max-width: 768px)': {
height: '150px',
},
}}
/>
<div
className="content"
css={{
padding: '16px',
'@media (max-width: 768px)': {
padding: '12px',
},
}}
>
<h3
className="title"
css={{
fontSize: '18px',
fontWeight: 'bold',
marginTop: 0,
marginBottom: '8px',
transition: 'color 0.2s',
}}
>
{props.title}
</h3>
<div
className="price"
css={{
fontSize: '20px',
color: 'green',
fontWeight: 'bold',
}}
>
${props.price}
</div>
<button
css={{
width: '100%',
padding: '12px',
backgroundColor: 'blue',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
transition: 'background-color 0.2s',
'&:active': {
transform: 'scale(0.98)',
},
}}
>
Add to Cart
</button>
</div>
</div>
)
}
CSS Mixin
For complex styling logic, create reusable CSS mixins:import { css } from 'remix/component'
function StyledButton() {
return (props: { variant: 'primary' | 'secondary' }) => (
<button
mix={[
css({
padding: '12px 24px',
borderRadius: '4px',
border: 'none',
cursor: 'pointer',
transition: 'all 0.2s',
'&:active': {
transform: 'scale(0.98)',
},
}),
props.variant === 'primary'
? css({
backgroundColor: 'blue',
color: 'white',
'&:hover': {
backgroundColor: 'darkblue',
},
})
: css({
backgroundColor: '#eee',
color: '#333',
'&:hover': {
backgroundColor: '#ddd',
},
}),
]}
>
Click me
</button>
)
}
Next Steps
- Events - Handle user interactions