DEV Community

Atilla Baspinar
Atilla Baspinar

Posted on

React: Styling (CSS)

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" />
Enter fullscreen mode Exit fullscreen mode

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" />;
}
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode
import styles from './Card.module.css';

export default function Card() {
  return <div className={`${styles.card} ${styles.primary}`}>Hello</div>;
}
Enter fullscreen mode Exit fullscreen mode

You can also apply a single module class:

<div className={styles.card}></div>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode
import styles from './Card.module.css';

export default function Card() {
  return <div className={styles.card}>Hello</div>;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode
import styles from './Card.module.css';

export default function Card() {
  return (
    <div className={styles.card}>
      <h2>Heading</h2>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
  • id selectors 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>;
}
Enter fullscreen mode Exit fullscreen mode

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;`;
Enter fullscreen mode Exit fullscreen mode

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" />;
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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 $invalid are useful to avoid sending styling props to the DOM
  • nested selectors like &:hover are 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)