CSS Modules
Learn how to use CSS Modules in Bini.js for component-scoped styling.
CSS Modules allow you to write component-scoped CSS without worrying about naming conflicts. Vite processes .module.css files automatically - no configuration needed.
.module.css is automatically processed as a CSS Module.Create a new project with CSS Modules using the --css-modules flag:
$ npx create-bini-app@latest my-app --css-modules
Or use the interactive prompt and select CSS Modules under the styling question:
? Select a styling solution:Tailwind CSS> CSS ModulesNone↑↓ navigate • ⏎ select
Or combine with TypeScript:
$ npx create-bini-app@latest my-app --css-modules --typescript
Basic Usage
Create a .module.css file next to your component and import it:
/* src/app/components/Button.module.css */
.button {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.primary {
background: #06b6d4;
color: black;
border: none;
}
.primary:hover {
background: #0891b2;
}// src/app/components/Button.tsx
import styles from './Button.module.css'
type ButtonProps = {
variant?: 'primary'
children: React.ReactNode
}
export function Button({ variant = 'primary', children }: ButtonProps) {
return (
<button className={`${styles.button} ${styles[variant]}`}>
{children}
</button>
)
}Vite rewrites every class name so it is unique to this file. The same .button in another module never collides:
Combining Classes
Combine multiple CSS Module classes using template literals:
/* src/app/components/Card.module.css */
.card {
background: #0a0a0a;
border: 1px solid #1e293b;
border-radius: 0.75rem;
padding: 1.5rem;
}
.featured {
border-color: #06b6d4;
}
.large {
padding: 2rem;
}// src/app/components/Card.tsx
import styles from './Card.module.css'
type CardProps = {
featured?: boolean
size?: 'normal' | 'large'
children: React.ReactNode
}
export function Card({ featured, size = 'normal', children }: CardProps) {
return (
<div className={`${styles.card} ${featured ? styles.featured : ''} ${size === 'large' ? styles.large : ''}`}>
{children}
</div>
)
}clsx or classnames library for cleaner conditional class composition.Using clsx for Cleaner Code
Install clsx for cleaner conditional classes:
$ npm install clsx
// src/app/components/Card.tsx
import clsx from 'clsx'
import styles from './Card.module.css'
type CardProps = {
featured?: boolean
size?: 'normal' | 'large'
children: React.ReactNode
}
export function Card({ featured, size = 'normal', children }: CardProps) {
return (
<div className={clsx(
styles.card,
featured && styles.featured,
size === 'large' && styles.large
)}>
{children}
</div>
)
}Global vs Local Scope
CSS Modules are locally scoped by default. Use :global to target global selectors:
/* src/app/components/Container.module.css */
.container {
max-width: 1200px;
margin: 0 auto;
}
.container :global(.heading) {
margin-bottom: 1rem;
}
:global(.dark) .container {
background: #000;
}// src/app/components/Container.tsx
import styles from './Container.module.css'
export function Container({ children }: { children: React.ReactNode }) {
return (
<div className={styles.container}>
<h2 className="heading">Global class, styled from the module</h2>
{children}
</div>
)
}Composing Classes
Use composes to reuse styles from other classes:
/* src/app/components/Form.module.css */
.baseInput {
width: 100%;
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
border: 1px solid #334155;
background: #0a0a0a;
color: white;
}
.textInput {
composes: baseInput;
}
.errorInput {
composes: baseInput;
border-color: #ef4444;
}import styles from './Form.module.css'
export function Form() {
return (
<form>
<input className={styles.textInput} placeholder="Name" />
<input className={styles.errorInput} placeholder="Email (invalid)" />
</form>
)
}CSS Variables in Modules
Use CSS variables for dynamic styling within modules:
/* src/app/components/Progress.module.css */
.progress {
height: 0.5rem;
overflow: hidden;
border-radius: 9999px;
background: #1e293b;
}
.bar {
height: 100%;
width: var(--progress);
background: linear-gradient(to right, #06b6d4, #3b82f6);
transition: width 0.3s ease;
}// src/app/components/Progress.tsx
import styles from './Progress.module.css'
type ProgressProps = {
value: number
max?: number
}
export function Progress({ value, max = 100 }: ProgressProps) {
const percentage = (value / max) * 100
return (
<div className={styles.progress}>
<div
className={styles.bar}
style={{ '--progress': `${percentage}%` } as React.CSSProperties}
/>
</div>
)
}Animations
Define animations in CSS Modules. @keyframes names are scoped to the module too:
/* src/app/components/Spinner.module.css */
.spinner {
width: 2rem;
height: 2rem;
border: 3px solid #1e293b;
border-top-color: #06b6d4;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}import styles from './Spinner.module.css'
export function Spinner() {
return <div className={styles.spinner} />
}Media Queries
Write responsive styles with media queries:
/* src/app/components/Grid.module.css */
.grid {
display: grid;
gap: 1rem;
grid-template-columns: 1fr;
}
@media (min-width: 640px) {
.grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.grid {
grid-template-columns: repeat(3, 1fr);
}
}// src/app/components/Grid.tsx
import styles from './Grid.module.css'
export function Grid({ children }: { children: React.ReactNode }) {
return <div className={styles.grid}>{children}</div>
}Complete Example
A full-featured modal component using CSS Modules:
/* src/app/components/Modal.module.css */
.overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
}
.modal {
background: #0a0a0a;
border: 1px solid #1e293b;
border-radius: 1rem;
padding: 1.5rem;
max-width: 500px;
width: 90%;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
}
.title {
font-size: 1.25rem;
font-weight: 600;
color: white;
}
.close {
background: transparent;
color: #94a3b8;
border: none;
cursor: pointer;
}
.close:hover {
color: white;
}
.body {
color: #94a3b8;
}// src/app/components/Modal.tsx
import styles from './Modal.module.css'
type ModalProps = {
isOpen: boolean
onClose: () => void
title: string
children: React.ReactNode
}
export function Modal({ isOpen, onClose, title, children }: ModalProps) {
if (!isOpen) return null
return (
<div className={styles.overlay} onClick={onClose}>
<div className={styles.modal} onClick={(e) => e.stopPropagation()}>
<div className={styles.header}>
<h2 className={styles.title}>{title}</h2>
<button className={styles.close} onClick={onClose}>✕</button>
</div>
<div className={styles.body}>{children}</div>
</div>
</div>
)
}