1) Static asset files and images
The way you use an image depends on where the file is stored.
Files inside public/
Files in the public folder are served from the app root. In Vite, that usually means they are exposed as /image.png.
<img src="/logo.png" alt="Logo" />
This works because the file is not processed by the bundler as a module. It is simply served as a static file.
Files inside src/ or other imported folders
If the image is part of your source code, import it first and then use the imported value as src.
import logo from './assets/logo.png';
export default function App() {
return <img src={logo} alt="App logo" />;
}
Why import it?
- the bundler can optimize and transform the file
- it can add hashed filenames for caching
- it can handle asset references reliably in JavaScript/TypeScript modules
- it keeps paths consistent with the rest of the build system
2) CSS
There are three common approaches: global CSS and CSS Modules and Frameworks (Tailwind etc.)
a) Global CSS (vanilla CSS)
If you import a CSS file, the styles are global by default.
import './App.css';
This is useful for resets, layout styles, or app-wide styling. But it has a trade-off:
- styles are shared across the whole project
- class names can collide between components
- a class added in one file can affect another component if the selector matches
This is normal in basic CSS setups, but it gets harder to manage as the app grows.
b) CSS Modules
CSS Modules are scoped to a component. The file name should end with .module.css.
/* Card.module.css */
.card {
border: 1px solid #ddd;
padding: 12px;
}
.primary {
background: #f5f5f5;
}
import styles from './Card.module.css';
export default function Card() {
return <div className={`${styles.card} ${styles.primary}`}>Hello</div>;
}
You can also apply a single module class:
<div className={styles.card}></div>
This pattern keeps class names local to the component and avoids collisions with classes from other files.
How CSS Modules handle id and tags
CSS Modules mainly protect class selectors, but the behavior depends on what you write in the stylesheet.
1. Class selectors are locally scoped
/* Card.module.css */
.card {
border: 1px solid #ddd;
}
import styles from './Card.module.css';
export default function Card() {
return <div className={styles.card}>Hello</div>;
}
This becomes something like a unique generated class name, so it is unlikely to conflict with another .card elsewhere in the app.
2. id selectors are not automatically scoped by CSS Modules
/* Card.module.css */
#title {
color: red;
}
This is still a global id selector unless you do something special in your CSS setup. In practice, CSS Modules do not rename id values the same way they do class names.
export default function Card() {
return <h2 id="title">Hello</h2>;
}
Because IDs are unique in the document, using them globally is usually not a good idea for component styling. Prefer classes for styling and reserve id for DOM anchors, labels, or JavaScript references.
3. Tag selectors are also global unless you scope them with a class
/* Card.module.css */
.card h2 {
color: blue;
}
import styles from './Card.module.css';
export default function Card() {
return (
<div className={styles.card}>
<h2>Heading</h2>
</div>
);
}
This selector is scoped relative to the generated class name, so it is still safer than a raw global h2 selector.
But this is not recommended as the main method of styling:
/* This is global and can affect every h2 in the app */
h2 {
color: green;
}
In component-based apps, it is better to style through classes and keep the structure specific to the component.
In short:
- class selectors in CSS Modules are locally scoped
-
idselectors remain global in practice - tag selectors can still affect many elements unless they are combined with a local class
- prefer
className={styles.someClass}for component styling
c) Styled Components
Styled Components is a popular CSS-in-JS approach. Instead of writing CSS in a separate file, you define a styled React element with a tagged template literal.
import styled from 'styled-components';
const Button = styled.button`
background: #2563eb;
color: white;
border: none;
border-radius: 8px;
padding: 10px 16px;
cursor: pointer;
`;
export default function App() {
return <Button>Click me</Button>;
}
This works because the styled.button function creates a React component whose CSS is attached to that component. The template literal is a tagged template, not a normal string.
A tagged template literal looks like this:
const message = `hello`;
const styledText = styled.div`color: red;`;
The important idea is that the CSS is passed as a template literal to the styled function, and the library turns it into component styles.
Dynamic styling with a transient prop
You can customize styles based on props.
import styled from 'styled-components';
const Input = styled.input<{ $invalid?: boolean }>`
border: 2px solid ${({ $invalid }) => ($invalid ? 'crimson' : '#cbd5e1')};
padding: 10px 12px;
border-radius: 6px;
color: ${({ $invalid }) => ($invalid ? 'crimson' : '#111827')};
`;
export default function App() {
return <Input $invalid={true} defaultValue="bad value" />;
}
Using $invalid is common in styled-components because the $ prefix tells the library to treat it as a transient prop. That means it is used for styling only and is not forwarded to the DOM as a normal HTML attribute.
Nested selectors and other features
Styled components support nested selectors and pseudo-classes.
import styled from 'styled-components';
const Card = styled.div`
background: white;
border: 1px solid #e5e7eb;
padding: 16px;
border-radius: 12px;
&:hover {
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
}
.title {
font-weight: 700;
color: #111827;
}
`;
export default function App() {
return (
<Card>
<p className="title">Styled Card</p>
</Card>
);
}
This is useful when a component needs more than just a simple class name, especially for hover states, nested elements, or variants.
Summary
- styled-components uses a tagged template literal syntax
- you define CSS directly inside a component definition
- props can control dynamic styles
- transient props like
$invalidare useful to avoid sending styling props to the DOM - nested selectors like
&:hoverare supported
This is a different styling model from CSS Modules, but it is popular because it keeps component styles close to the component itself.
Top comments (0)