Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
Sichere ProgrammierungLandlock LSM: Sandbox-Linux ohne Root-Rechte sichern(22.09.2026 um 02:00 Uhr)
Sichere ProgrammierungQuarkus + GraalVM Advanced Obfuscation(22.09.2026 um 02:00 Uhr)
Sichere Programmierung06 — Chat Works. Does the Agent Actually Retrieve Memory?(22.09.2026 um 02:03 Uhr)
Sichere ProgrammierungMCP Debate: Token Tax, Context Bloat, and What Devs Can Do(22.09.2026 um 02:03 Uhr)
Sichere ProgrammierungI gave my AI agent a kill switch tied to its own bank balance(22.09.2026 um 02:08 Uhr)
Sichere ProgrammierungLandlock LSM: Sandbox-Linux ohne Root-Rechte sichern(22.09.2026 um 02:00 Uhr)
Sichere ProgrammierungQuarkus + GraalVM Advanced Obfuscation(22.09.2026 um 02:00 Uhr)
Sichere Programmierung06 — Chat Works. Does the Agent Actually Retrieve Memory?(22.09.2026 um 02:03 Uhr)
Sichere ProgrammierungMCP Debate: Token Tax, Context Bloat, and What Devs Can Do(22.09.2026 um 02:03 Uhr)
Sichere ProgrammierungI gave my AI agent a kill switch tied to its own bank balance(22.09.2026 um 02:08 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

From CSS Modules to a Tiny Design System: Themeable Buttons in React

In this second part of the series that began with “Styling Your First React Component — A Gentle Introduction,” we’ll turn a simple button into a tiny, themeable design‑system primitive using CSS Modules, CSS variables, and a few ergonomic …

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!

In this second part of the series that began with “Styling Your First React Component — A Gentle Introduction,” we’ll turn a simple button into a tiny, themeable design‑system primitive using CSS Modules, CSS variables, and a few ergonomic React patterns.




Hero: Themeable React button component by Elram Gavrieli

Alt: “Hero cover showing a minimal React logo and a glowing button — artwork by Elram Gavrieli.”







What you'll build




  • A Button component with size, variant (filled / outline / ghost), and state (loading / disabled).

  • Theme support (light & dark) via CSS variables.

  • Accessible semantics and keyboard focus.

  • A tiny API that’s easy to reuse across apps.



A finished demo looks like this:




<Button>Default</Button>
<Button variant="primary">Primary</Button>
<Button variant="outline">Outline</Button>
<Button size="lg" loading>Saving…</Button>












1) Project setup (Vite + React)






# create a new React app with Vite
npm create vite@latest react-buttons -- --template react
cd react-buttons
npm install
npm run dev






Enable CSS Modules by naming your stylesheet *.module.css. We’ll keep everything co‑located.




src/
components/
Button/
Button.jsx
Button.module.css
App.jsx
main.jsx
theme.css












2) Theme tokens with CSS variables



Create global CSS variables once, use everywhere. Add this near the top of src/main.jsx or in a global stylesheet imported by it.




/* theme.css */
:root {
/* spacing */
--space-1: .25rem;
--space-2: .5rem;
--space-3: .75rem;
--space-4: 1rem;

/* radii, duration */
--radius: .625rem;
--speed: .18s;

/* dark-ish theme */
--bg: #0b0f14;
--panel: #121822;
--text: #e8eef6;
--muted: #9fb3c8;

--brand: #4cc2ff;
--brand-600: #3aa2d4;

--ok: #3ddc97;
--danger: #ff6b6b;
--ring: #66d9ff;
}

/* optional light theme */
.theme-light {
--bg: #ffffff;
--panel: #f5f7fb;
--text: #0b1020;
--muted: #50607a;
}






Import the theme once in your app entry:




// main.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import "./theme.css";

ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
{/* swap theme-dark / theme-light to test */}
<div className="theme-dark">
<App />
</div>
</React.StrictMode>
);







Tip: Toggle theme-dark / theme-light on the wrapper to switch themes.










3) The Button component






Styles






/* components/Button/Button.module.css */
.button {
--btn-bg: var(--panel);
--btn-fg: var(--text);
--btn-border: transparent;

display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
padding: calc(var(--space-2) + 2px) calc(var(--space-4) + 2px);
border-radius: var(--radius);
border: 1px solid var(--btn-border);
background: var(--btn-bg);
color: var(--btn-fg);
font-weight: 600;
line-height: 1;
cursor: pointer;
transition: transform var(--speed) ease, box-shadow var(--speed) ease, background var(--speed) ease;
box-shadow: 0 1px 0 rgba(0,0,0,.25), 0 8px 18px rgba(0,0,0,.25);
text-decoration: none;
}

.button:hover { transform: translateY(-1px); }
.button:active { transform: translateY(0); }

.button:focus-visible {
outline: none;
box-shadow:
0 0 0 2px color-mix(in oklab, white 10%, transparent),
0 0 0 4px var(--ring);
}

/* sizes */
.sm { padding: var(--space-2) var(--space-3); font-size: .9rem; }
.md { padding: calc(var(--space-2) + 2px) calc(var(--space-4) + 2px); font-size: 1rem; }
.lg { padding: calc(var(--space-3) + 2px) calc(var(--space-4) * 1.5); font-size: 1.1rem; }

/* variants */
.primary { --btn-bg: var(--brand); --btn-fg: #001018; }
.primary:hover { --btn-bg: var(--brand-600); }

.outline { --btn-bg: transparent; --btn-border: color-mix(in oklab, var(--text) 16%, transparent); }
.outline:hover { --btn-border: color-mix(in oklab, var(--text) 28%, transparent); }

.ghost { --btn-bg: transparent; --btn-fg: var(--text); box-shadow: none; }

/* states */
.disabled,
.button[aria-disabled="true"] {
opacity: .6;
cursor: not-allowed;
transform: none;
box-shadow: none;
}

.loading {
pointer-events: none;
}

.spinner {
width: 1em;
height: 1em;
border-radius: 999px;
border: 2px solid color-mix(in oklab, var(--text) 30%, transparent);
border-top-color: var(--text);
animation: spin .9s linear infinite;
}

@keyframes spin { to { transform: rotate(360deg); } }









Component






// components/Button/Button.jsx
import cls from "./Button.module.css";

export default function Button({
as: Comp = "button",
children,
variant = "primary",
size = "md",
loading = false,
disabled = false,
onClick,
href,
...rest
}) {
const isDisabled = disabled || loading;
const className = [
cls.button,
cls[variant],
cls[size],
loading ? cls.loading : "",
isDisabled ? cls.disabled : "",
rest.className || ""
].join(" ").trim();

const common = {
className,
"aria-disabled": isDisabled || undefined,
onClick: isDisabled ? undefined : onClick,
...rest
};

if (href && Comp === "button") {
Comp = "a";
common.href = href;
common.role = "button";
}

return (
<Comp {...common}>
{loading && <span className={cls.spinner} aria-hidden="true" />}
<span>{children}</span>
</Comp>
);
}












4) Use it






// App.jsx
import Button from "./components/Button/Button.jsx";

export default function App() {
return (
<main style={{ minHeight:"100dvh", background:"var(--bg)", color:"var(--text)", display:"grid", placeItems:"center", padding:"2rem" }}>
<div style={{ display:"grid", gap:"1rem", background:"var(--panel)", padding:"2rem", borderRadius:"1rem" }}>
<h1 style={{ margin:0 }}>Themeable Buttons</h1>

<div style={{ display:"flex", gap:".75rem", flexWrap:"wrap" }}>
<Button>Default</Button>
<Button variant="primary">Primary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
</div>

<div style={{ display:"flex", gap:".75rem", flexWrap:"wrap" }}>
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
</div>

<div style={{ display:"flex", gap:".75rem", flexWrap:"wrap" }}>
<Button loading>Saving…</Button>
<Button disabled>Disabled</Button>
<Button href="https://dev.to" variant="outline">As Link</Button>
</div>
</div>
</main>
);
}












5) Accessibility checklist




  • Uses :focus-visible ring for keyboard users.

  • When href is provided, the component renders an <a> with role="button" for consistency.


  • aria-disabled="true" communicates the disabled state without breaking links.

  • Spinner has aria-hidden="true" so screen readers don’t announce it twice.









6) Theming in one line



Because visual tokens live in CSS variables, you can theme by flipping a single class on a wrapper:




<div class="theme-light">
<!-- app here -->
</div>






Try adjusting the --brand and --panel variables to match your brand palette. No JS changes required.









7) Where to go next




  • Extract tokens into a dedicated tokens.css and generate light/dark/system themes.

  • Add an icon slot prop to the Button that accepts a React node.

  • Build Input, Badge, and Toast with the same approach—you’ve got a design system starter.









Downloadable assets




  • Cover image placeholder: replace the link at the top with your uploaded asset. Suggested alt text: “Themeable React button component by Elram Gavrieli.”

  • File names for social previews: elram-gavrieli-react-themeable-buttons-cover.jpg









Final thoughts



You don’t need Tailwind, Styled Components, or a full UI kit to get ergonomic, themeable components. CSS Modules + CSS Variables keep things portable and easy to understand—perfect for small teams and side projects.



If you found this helpful, consider following the series and say hi in the comments—what component should we theme next?

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten From CSS Modules to a Tiny Design System: Themeable Buttons in React

Thematisch verwandte Begriffe: From, Modules, Tiny, Design · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-49449 | Joplin is an open source note-taking and to-do application that organise…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick