Plain CSS

Use plain CSS in Bini.js - import only what you need, where you need it. No framework required.

Plain CSS in Bini.js

Bini.js supports plain .css files natively via Vite. No config needed - just import a .css file and it works. This page covers plain CSS only.

>_Terminal
$ npx create-bini-app@latest my-app --none

Or use the interactive prompt and select None under the styling question:

>_Terminal
? Select a styling solution:
Tailwind CSS
CSS Modules
> None
 
↑↓ navigate • ⏎ select

Global CSS

For base styles, resets, and utilities that should apply everywhere, import a global stylesheet in your root layout:

src/app/globals.css
/* src/app/globals.css - base reset and variables */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

:root {
  --bg: #ffffff;
  --text: #0a0a0a;
  --border: #e5e5e5;
}

body {
  font-family: system-ui, -apple-system, sans-serif;
  background: var(--bg);
  color: var(--text);
  line-height: 1.6;
}
src/app/layout.tsx
// src/app/layout.tsx - root layout
import './globals.css'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}
Keep global CSS minimal - only resets, CSS variables, and truly global utilities. Route and component specific styles should be imported closer to where they are used.

CSS for Specific Routes

Import CSS only for the routes that need it. This keeps bundles small and avoids loading unused styles. Each route can have its own stylesheet:

src/app/(marketing)/page.css
/* src/app/(marketing)/page.css - only loaded for marketing route */
.hero {
  min-height: 80vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
}

.hero h1 {
  font-size: 3rem;
  font-weight: 700;
  letter-spacing: -0.02em;
}
src/app/(marketing)/page.tsx
// src/app/(marketing)/page.tsx
import './page.css'

export default function MarketingPage() {
  return (
    <div className="hero">
      <h1>Welcome to Bini.js</h1>
      <p>Build fast, ship faster</p>
    </div>
  )
}
src/app/dashboard/page.css
/* src/app/dashboard/page.css - only for dashboard */
.dashboard-grid {
  display: grid;
  grid-template-columns: 240px 1fr;
  gap: 1.5rem;
}

.sidebar {
  border-right: 1px solid var(--border);
  padding-right: 1.5rem;
}
src/app/dashboard/layout.tsx
// src/app/dashboard/layout.tsx - layout level CSS for dashboard
import './page.css'

export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  return <div className="dashboard-grid">{children}</div>
}
Route-level CSS is code-split automatically by Vite. A user visiting / will not download dashboard/page.css. Import CSS as close as possible to the route that uses it.

Blog Layout Example - Scoped CSS

If your blogs layout has a blog.css, that CSS only applies to routes inside the blog folder. Other routes do not get it:

src/app/blog/blog.css
/* src/app/blog/blog.css - only for /blog/* */
.blog-wrapper {
  max-width: 720px;
  margin: 0 auto;
  padding: 2rem 1rem;
  line-height: 1.7;
}

.blog-wrapper h1 {
  font-size: 2rem;
  font-weight: 700;
}

.blog-wrapper article {
  color: var(--text);
}
src/app/blog/layout.tsx
// src/app/blog/layout.tsx - import here, scoped to /blog only
import './blog.css'

export default function BlogLayout({ children }: { children: React.ReactNode }) {
  return <div className="blog-wrapper">{children}</div>
}
Gets blog.cssLoaded
/blog
/blog/my-post
/blog/category/tech
Does NOT get blog.cssNot loaded
/
/dashboard
/about
/docs
How it works: Vite code-splits by route. When you visit /, Vite loads only globals.css + /(marketing)/page.css. When you visit /blog, the blog/layout.tsx chain is loaded, so blog.css is included. If you import blog.css in the root src/app/layout.tsx instead, every route would get it - avoid that for scoped styles.

Component CSS

For reusable components, keep a plain .css file next to the component. This still works without CSS Modules - just use clear naming to avoid conflicts:

src/app/components/Button.css
/* src/app/components/Button.css */
.btn {
  padding: 0.5rem 1rem;
  border-radius: 0.5rem;
  font-weight: 500;
  border: 1px solid var(--border);
  cursor: pointer;
  transition: background 0.2s, color 0.2s;
}

.btn-primary {
  background: black;
  color: white;
}

.btn-primary:hover {
  background: #222;
}
src/app/components/Button.tsx
// src/app/components/Button.tsx
import './Button.css'

export function Button({ variant = 'primary', children, ...props }: any) {
  return <button className={`btn btn-${variant}`} {...props}>{children}</button>
}
If you need scoped styles to avoid conflicts, use CSS Modules (.module.css) instead. For plain CSS, use BEM or prefixed class names like btn-, card-.

External Stylesheets

Import CSS from npm packages only in routes that need them:

src/app/docs/page.tsx
// src/app/docs/page.tsx - only docs needs syntax highlighting
import 'prismjs/themes/prism.css'

export default function DocsPage() {
  return <article>...</article>
}
src/app/blog/page.tsx
// src/app/blog/page.tsx - only blog needs markdown styles
import './markdown.css'

export default function BlogPage() {
  return <div className="markdown-body">...</div>
}
Do not import external CSS globally if only one route needs it. Import it in the specific route or layout to keep other routes lean.

CSS Ordering

CSS is applied in the order you import it. Keep a consistent order:

src/app/layout.tsx
// layout.tsx - order matters
import './globals.css'      // 1. Base reset and variables first
import './theme.css'        // 2. Theme and utilities
// Route or component CSS comes after, imported inside page.tsx or component.tsx
src/app/dashboard/page.tsx
// src/app/dashboard/page.tsx
import './page.css'  // 3. Route-specific CSS - loaded only for this route

export default function DashboardPage() {
  return <div className="dashboard">...</div>
}
Global to specific: globals.css first, then layout CSS, then route CSS, then component CSS. This avoids specificity surprises.

CSS Variables

Use CSS variables for theming - define them once in global CSS, use everywhere:

src/app/globals.css
/* src/app/globals.css */
:root {
  --bg: #ffffff;
  --text: #0a0a0a;
  --border: #e5e5e5;
  --radius: 0.5rem;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #000000;
    --text: #fafafa;
    --border: #262626;
  }
}

body {
  background: var(--bg);
  color: var(--text);
}

Sass/SCSS

Vite supports Sass out of the box. Install and use .scss only where needed:

>_Terminal
$ npm install -D sass
src/app/dashboard/page.scss
/* src/app/dashboard/page.scss - only for dashboard */
.dashboard {
  display: grid;
  gap: 1rem;

  .card {
    padding: 1rem;
    border: 1px solid var(--border);
    border-radius: var(--radius);

    &:hover {
      border-color: black;
    }
  }
}
src/app/dashboard/page.tsx
import './page.scss'  // only dashboard loads this

export default function DashboardPage() {
  return <div className="dashboard">...</div>
}

CSS-in-JS Alternative

If you prefer CSS-in-JS, use plain CSS setup (--none) and install your library. Only load it in routes that need it:

>_Terminal
$ npm install styled-components
src/app/components/StyledButton.tsx
// src/app/components/StyledButton.tsx
import styled from 'styled-components'

const Button = styled.button`
  padding: 0.5rem 1rem;
  border-radius: 0.5rem;
  background: black;
  color: white;
`

export function StyledButton({ children }: any) {
  return <Button>{children}</Button>
}
For most projects, plain CSS with route-level imports is simpler and faster. Use CSS-in-JS only when you need dynamic theming based on props.
Was this helpful?