| name | styling |
| description | Guide de styling avec CapUI (@cap-collectif/ui). Couvre layout, spacing, couleurs, responsive, theming et composants UI. Ne pas utiliser styled-components. |
Styling avec CapUI
Guide pour styler les composants avec @cap-collectif/ui. Ne pas utiliser styled-components - privilegier les props CapUI et le prop sx pour tous les styles.
Imports
import {
Box, Flex, Grid, Text, Heading,
Button, Icon, Avatar, Card, Modal,
Accordion, InfoMessage, Spinner, Tooltip
} from '@cap-collectif/ui'
import {
CapUIIcon,
CapUIIconSize,
CapUIFontSize,
CapUIFontWeight,
CapUILineHeight,
CapUIModalSize,
CapUIAccordionColor,
CapUIShadow,
CapUIRadius
} from '@cap-collectif/ui'
import { FieldInput, FormControl } from '@cap-collectif/form'
Layout
Flex (conteneur flexible)
<Flex
direction="column"
align="center"
justify="space-between"
wrap="wrap"
gap={4}
spacing="md"
>
<Box>Item 1</Box>
<Box>Item 2</Box>
</Flex>
Box (conteneur generique)
<Box
as="section"
p={4}
m={2}
bg="gray.100"
borderRadius="normal"
boxShadow={CapUIShadow.Small}
position="relative"
width="100%"
maxWidth="600px"
>
Contenu
</Box>
Grid
<Grid
templateColumns="repeat(3, 1fr)"
templateRows="auto"
gap={4}
autoFlow="row"
>
<Box>Cell 1</Box>
<Box>Cell 2</Box>
<Box>Cell 3</Box>
</Grid>
<Grid
templateColumns={{ base: '1fr', md: 'repeat(2, 1fr)', lg: 'repeat(3, 1fr)' }}
gap={{ base: 2, md: 4 }}
>
{/* ... */}
</Grid>
Spacing (Padding & Margin)
Tokens semantiques (privilegier)
| Token | Valeur | Usage |
|---|
"0" | 0px | Aucun espace |
"px" | 1px | Bordures fines |
"xxs" | 4px | Tres petit |
"xs" | 8px | Petit |
"sm" | 12px | Moyen-petit |
"md" | 16px | Standard |
"lg" | 24px | Grand |
"xl" | 32px | Tres grand |
"xxl" | 48px | Extra large |
"xxxl" | 64px | Enorme |
Props de spacing
<Box p="md" />
<Box px="lg" />
<Box py="sm" />
<Box pt="xs" pb="md" />
<Box pl={4} pr={4} />
<Box m="md" />
<Box mx="auto" />
<Box mt="lg" mb="sm" />
<Box ml="auto" />
<Box mt="-sm" />
Couleurs
Palette
Préférer les tokens sémantiques aux valeurs numériques (exemple : primary.500 → primary.base).
primary.50
primary.100
primary.200
primary.300
primary.400
primary.500
primary.600
primary.700
primary.800
primary.900
gray.50 → gray.900
blue.100 → blue.900
red.100 → red.900
green.100 → green.900
yellow.100 → yellow.900
Usage
<Box
bg="gray.100"
color="gray.900"
borderColor="gray.300"
/>
<Text color="primary.600">Texte principal</Text>
<Text color="red.500">Erreur</Text>
<Text color="gray.500">Texte secondaire</Text>
Typographie
Text
<Text
fontSize={CapUIFontSize.BodyRegular}
fontWeight={CapUIFontWeight.Semibold}
lineHeight={CapUILineHeight.M}
color="gray.700"
textAlign="center"
truncate
lineClamp={2}
>
Contenu texte
</Text>
Heading
<Heading
as="h1"
color="blue.900"
mb="md"
>
Titre principal
</Heading>
Responsive Design
Breakpoints
| Breakpoint | Valeur | Usage |
|---|
base | 0px+ | Mobile first (defaut) |
sm | 480px+ | Petit mobile |
md | 768px+ | Tablette |
lg | 992px+ | Desktop |
xl | 1280px+ | Grand ecran |
Props responsive
<Box
p={{ base: 'sm', md: 'lg', lg: 'xl' }}
display={{ base: 'none', md: 'block' }}
flexDirection={{ base: 'column', lg: 'row' }}
width={{ base: '100%', md: '50%', lg: '33%' }}
/>
<Grid
templateColumns={{
base: '1fr',
md: 'repeat(2, 1fr)',
lg: 'repeat(3, 1fr)'
}}
/>
Hook useIsMobile
import useIsMobile from '@hooks/useIsMobile'
const MyComponent = () => {
const isMobile = useIsMobile()
return isMobile ? <MobileView /> : <DesktopView />
}
Affichage conditionnel
<Box display={{ base: 'none', md: 'block' }}>
Desktop only
</Box>
<Box display={{ base: 'block', md: 'none' }}>
Mobile only
</Box>
Prop sx (styles custom)
Pour les styles non couverts par les props, utiliser sx :
<Box
sx={{
'&::before': {
content: '""',
position: 'absolute',
},
transition: 'all 0.2s ease',
transform: 'translateY(-2px)',
backgroundImage: 'linear-gradient(to right, #000, #fff)',
clipPath: 'polygon(0 0, 100% 0, 100% 80%, 0 100%)',
}}
/>
Etats interactifs
<Flex
as="button"
cursor="pointer"
_hover={{
bg: 'gray.100',
color: 'primary.600',
transform: 'translateY(-1px)',
}}
_focus={{
outline: 'none',
boxShadow: 'outline',
}}
_active={{
bg: 'gray.200',
transform: 'translateY(0)',
}}
_disabled={{
opacity: 0.5,
cursor: 'not-allowed',
}}
>
Clickable
</Flex>
Composants UI
Button
<Button
variant="primary"
variantSize="medium"
variantColor="primary"
leftIcon={CapUIIcon.Add}
rightIcon={CapUIIcon.ArrowRight}
isLoading={isSubmitting}
disabled={!isValid}
onClick={handleClick}
type="submit"
>
{intl.formatMessage({ id: 'global.save' })}
</Button>
Modal
<Modal
show={isOpen}
onClose={onClose}
size={CapUIModalSize.Md}
ariaLabel="Modal title"
fullSizeOnMobile
hideOnClickOutside={false}
>
<Modal.Header>
<Heading as="h4" color="blue.900">Titre</Heading>
</Modal.Header>
<Modal.Body spacing={4}>
{/* Contenu */}
</Modal.Body>
<Modal.Footer>
<Button variant="tertiary" onClick={onClose}>
{intl.formatMessage({ id: 'global.cancel' })}
</Button>
<Button variant="primary" onClick={handleSubmit}>
{intl.formatMessage({ id: 'global.confirm' })}
</Button>
</Modal.Footer>
</Modal>
Card
import {
Card, CardCover, CardCoverImage,
CardCoverPlaceholder, CardContent, CardTagList
} from '@cap-collectif/ui'
<Card
format="vertical"
sx={{ boxShadow: CapUIShadow.Small }}
>
<CardCover>
{imageUrl ? (
<CardCoverImage src={imageUrl} alt={title} />
) : (
<CardCoverPlaceholder icon={CapUIIcon.FolderO} color="primary.base" />
)}
</CardCover>
<CardContent
primaryInfo={title}
secondaryInfo={description}
href={url}
primaryInfoTag="h2"
>
<CardTagList>
<Text fontSize="sm" color="gray.500">{date}</Text>
</CardTagList>
</CardContent>
</Card>
Icon
<Icon
name={CapUIIcon.Pencil}
size={CapUIIconSize.Md}
color="gray.500"
/>
CapUIIcon.Add
CapUIIcon.Trash
CapUIIcon.Pencil
CapUIIcon.Cross
CapUIIcon.Check
CapUIIcon.ArrowRight
CapUIIcon.User
CapUIIcon.Cog
CapUIIcon.Search
InfoMessage
<InfoMessage
variant="warning"
mt="sm"
>
<InfoMessage.Title>
{intl.formatMessage({ id: 'warning.title' })}
</InfoMessage.Title>
<InfoMessage.Content>
{intl.formatMessage({ id: 'warning.content' })}
</InfoMessage.Content>
</InfoMessage>
Accordion
<Accordion
color={CapUIAccordionColor.Primary}
allowMultiple
defaultAccordion={['section-1']}
>
<Accordion.Item id="section-1">
<Accordion.Button p={0}>
<Text fontWeight={CapUIFontWeight.Semibold}>
Section 1
</Text>
</Accordion.Button>
<Accordion.Panel>
Contenu de la section
</Accordion.Panel>
</Accordion.Item>
</Accordion>
Patterns recommandes
Conteneur de page
<Box maxWidth="1200px" mx="auto" px={{ base: 'md', lg: 'xl' }} py="lg">
{}
</Box>
Liste avec separateurs
<Flex direction="column" gap={0}>
{items.map((item, index) => (
<Box
key={item.id}
py="md"
borderBottomWidth={index < items.length - 1 ? '1px' : 0}
borderColor="gray.200"
>
{item.name}
</Box>
))}
</Flex>
Formulaire vertical
<Flex direction="column" gap={4} maxWidth="500px">
<FormControl name="title" control={control} isRequired>
<FormControl.Label>Titre</FormControl.Label>
<FieldInput name="title" control={control} type="text" />
</FormControl>
<FormControl name="description" control={control}>
<FormControl.Label>Description</FormControl.Label>
<FieldInput name="description" control={control} type="textarea" />
</FormControl>
<Button type="submit" variant="primary" alignSelf="flex-end">
Enregistrer
</Button>
</Flex>
Centrage vertical et horizontal
<Flex
height="100vh"
align="center"
justify="center"
>
<Box>Contenu centre</Box>
</Flex>
Bonnes pratiques
A faire
- Tokens semantiques : Utiliser
"md", "lg" plutot que des valeurs numeriques
- Props CapUI : Privilegier les props directes (
p, m, bg) avant sx
- Responsive mobile-first : Commencer par
base puis md, lg
- Composants CapUI : Utiliser Button, Card, Modal au lieu de recreer
- Spacing coherent : Utiliser les memes tokens dans tout le projet
A eviter
- styled-components : Ne pas utiliser, migrer vers CapUI
- CSS inline : Eviter
style={{}}, utiliser les props ou sx
- Valeurs magiques : Pas de
padding: '17px', utiliser les tokens
- !important : Jamais necessaire avec CapUI
- Classes CSS : Eviter sauf pour integration externe (Leaflet, etc.)
Migration depuis styled-components
const Container = styled.div`
display: flex;
padding: 16px;
background: #f5f5f5;
&:hover {
background: #e0e0e0;
}
`
<Flex
p="md"
bg="gray.100"
_hover={{ bg: 'gray.200' }}
>
{}
</Flex>
Exemples du projet
Checklist