Tailwind CSS Framework - Komponenten
In diesem Artikel zeige ich, wie man dem Tailwind CSS Framework wiederverwendbare Komponenten mit variablen CSS Klassen erstellen kann.
Inhaltsverzeichnis
Möchten Sie wiederverwendbare Komponenten in Ihrer Web-Anwendung mit eigenen Parametern und CSS-Klassen erstellen? In diesem Artikel werfen wir einen Blick darauf, wie man das Framework Tailwind CSS zusammen mit Next.js nutzen kann, um modulare Komponenten zu erstellen, die einfach zu warten und anzupassen sind. Selbstverständlich sollte dieses Konzept auch für andere Frameworks wie Gatsby.js oder die Bibliothek React.js funktionieren.
Next.js (React) Projekt mit Tailwind CSS
Ausgangspunkt ist ein Next.js Projekt, meine package.json
sieht zu diesem Zeitpunkt so aus:
{
"scripts": {
"dev": "next dev"
},
"dependencies": {
"next": "^13.1.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"autoprefixer": "^10.4.13",
"postcss": "^8.4.20",
"prettier-plugin-tailwindcss": "^0.2.1",
"prettier": "^2.8.1",
"tailwindcss": "^3.2.4"
}
}
Im src
- Ordner befindet sich ein pages
- Ordner mit einer \_app.jsx
und einer index.jsx
. Die index.jsx
sieht so aus:
const IndexPage = () => (
<div className="m-8">
<h1 className="mb-6">Hallo</h1>
</div>
);
export default IndexPage;
Ich möchte einen Button zur Seite hinzufügen:
const IndexPage = () => (
<div className="m-8">
<h1 className="mb-6">Hallo</h1>
<button> Das ist ein Button </button>
</div>
);
export default IndexPage;
Komponente erstellen
Nun wird es aber unübersichtlich im HTML Teil der Javascript Datei, wenn ich viele Buttons verwenden möchte. An jedes Element müssten CSS Klassen hinzugefügt werden, um sie zu stylen. Großer Vorteil von React gegenüber reinem HTML ist, dass Komponenten wiederverwendet werden können. Also verschiebe ich den Button in eine Komponente, um einheitliche Buttons zu definieren. Dazu erstelle ich im src Ordner einen neuen components Ordner. Und darin eine Button.jsx
Datei:
const Button = () => {
return <button>Das ist ein Button</button>;
};
export default Button;
Diesen Button nutze ich in der Index.jsx
Datei.
import Button from "../components/Button";
const IndexPage = () => (
<div className="m-8">
<h1 className="mb-6">Hallo</h1>
<Button />
</div>
);
export default IndexPage;
Gut sieht es nicht aus, aber der Button ist da.
Children übergeben
Das Problem an dieser Stelle ist, dass ich den Button nicht mit Text befüllen kann. Ich möchte den Button mit einem Text versehen, der in der Index.jsx
Datei definiert wird. Dazu muss ich die Button.jsx
Datei anpassen:
const Button = ({ children }) => {
return <button>{children}</button>;
};
export default Button;
Jetzt kann ich den Button mit einem Text befüllen:
import Button from "../components/Button";
const IndexPage = () => (
<div className="m-8">
<h1 className="mb-6">Hallo</h1>
<Button>Ein Button</Button>
</div>
);
export default IndexPage;
Button stylen
Nun möchte ich den Button stylen. Dazu muss ich die Button.jsx
Datei anpassen:
const Button = ({ children }) => {
return <button className="rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700">{children}</button>;
};
export default Button;
Varianten mit Tailwind CSS
Nun möchte ich einen weiteren Button erstellen, der aber eine andere Farbe hat. Dazu muss ich die Button.jsx
Datei anpassen:
const Button = ({ children, variant }) => {
return (
<button
className={`rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700 ${
variant === "secondary" ? "bg-red-500 hover:bg-red-700" : ""
}`}>
{children}
</button>
);
};
export default Button;
Jetzt kann ich den Button in der index.js mit einer Variante versehen:
import Button from "../components/Button";
const IndexPage = () => (
<div className="m-8">
<h1 className="mb-6">Hallo</h1>
<Button className="mr-4">Ein Button</Button>
<Button variant="secondary">Ein Button</Button>
</div>
);
export default IndexPage;
Klassennamen übergeben
Das Problem an dieser Stelle ist, dass ich zwar beim ersten Button ein margin right hinzugefügt habe, das aber keine Wirkung hat. Das liegt daran, dass die Button.jsx
Datei keine Klassennamen von der Index.jsx
Datei erhält. Ich muss die Button.jsx
Datei anpassen, damit ich die className
übergeben kann:
const Button = ({ children, variant, className }) => {
return (
<button
className={`rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700 ${
variant === "secondary" ? "bg-red-500 hover:bg-red-700" : ""
} ${className}`}>
{children}
</button>
);
};
export default Button;
Jetzt kann ich an meinen Button Klassennamen übergeben:
import Button from "../components/Button";
const IndexPage = () => (
<div className="m-8">
<h1 className="mb-6">Hallo</h1>
<Button className="mr-4">Ein Button</Button>
<Button variant="secondary">Ein Button</Button>
</div>
);
export default IndexPage;
Size übergeben
Nun möchte ich den Button mit einer Größe versehen. Dazu muss ich die Button.jsx
Datei anpassen:
const Button = ({ children, variant, className, size }) => {
return (
<button
className={`rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700 ${
variant === "secondary" ? "bg-red-500 hover:bg-red-700" : ""
} ${size === "small" ? "px-2 py-1 text-xs" : size === "large" ? "px-6 py-3" : ""} ${className}`}>
{children}
</button>
);
};
export default Button;
Jetzt kann ich den Button mit einer Variante versehen:
import Button from "../components/Button";
const IndexPage = () => (
<div className="m-8">
<h1 className="mb-6">Hallo</h1>
<Button className="mr-4">Ein Button</Button>
<Button variant="secondary" className="mr-4">
Ein Button
</Button>
<Button size="small" className="mr-4">
Ein Button
</Button>
<Button size="large">Ein Button</Button>
</div>
);
export default IndexPage;
Weitere Props übergeben
Nun möchte ich dem Button auch alle anderen props übergeben können. Dazu muss ich die Button.jsx
Datei anpassen:
const Button = ({ children, variant, className, size, ...props }) => {
return (
<button
{...props}
className={`rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700 ${
variant === "secondary" ? "bg-red-500 hover:bg-red-700" : ""
} ${size === "small" ? "px-2 py-1 text-xs" : size === "large" ? "px-6 py-3" : ""} ${className}`}>
{children}
</button>
);
};
Jetzt kann ich den Button mit einer Funktion versehen:
import Button from "../components/Button";
const IndexPage = () => (
<div className="m-8">
<h1 className="mb-6">Hallo</h1>
<Button className="mr-4">Ein Button</Button>
<Button variant="secondary" className="mr-4">
Ein Button
</Button>
<Button className="mr-4" onClick={() => alert("Hallo")}>
Ein Button
</Button>
<Button variant="secondary" onClick={() => alert("Hallo")}>
Ein Button
</Button>
</div>
);
export default IndexPage;
Damit kann ich nun alle anderen props übergeben, die ich möchte.
Optimieren
Das Ganze könnte man noch optimieren, indem man die Klassen auslagert. Außerdem kann man bei Parametern, die nur zwei Zustände haben, diese direkt benutzen. Dazu muss ich die Button.jsx
Datei anpassen:
const Button = ({ children, className, secondary, size, shadow, ...props }) => {
const classes = [
"text-white font-bold rounded",
secondary ? "bg-red-500 hover:bg-red-700" : "bg-blue-500 hover:bg-blue-700 ",
size === "small" ? "py-1 px-2 text-xs" : size === "large" ? "py-3 px-6" : "py-2 px-4",
shadow && "shadow-sm hover:shadow",
className,
];
return (
<button {...props} className={classes.join(" ")}>
{children}
</button>
);
};
export default Button;
Jetzt habe ich einen modularen Button, denn ich in der Index.jsx
Datei verwenden kann:
import Button from "../components/Button";
const IndexPage = () => (
<div className="m-8">
<h1 className="mb-6">Hallo</h1>
<Button className="mr-4">Ein Button</Button>
<Button secondary className="mr-4">
Ein Button
</Button>
<Button className="mr-4" onClick={() => alert("Hallo")}>
Ein Button
</Button>
<Button secondary className="mr-4 bg-indigo-500" onClick={() => alert("Hallo")}>
Ein Button
</Button>
<Button secondary shadow>
Ein Button
</Button>
</div>
);
export default IndexPage;
Zur besseren Leserlichkeit lassen sich die genutzen Klassen auch aus der Button Funktion lösen. Dazu muss ich die Button.jsx
Datei anpassen:
const classes = {
base: "text-white font-bold rounded",
primary: "bg-blue-500 hover:bg-blue-700",
secondary: "bg-red-500 hover:bg-red-700",
small: "py-1 px-2 text-xs",
large: "py-3 px-6",
default: "py-2 px-4",
shadow: "shadow-sm hover:shadow",
};
const Button = ({ children, className, secondary, size, shadow, ...props }) => {
const buttonClasses = [
classes.base,
secondary ? classes.secondary : classes.primary,
size === "small" ? classes.small : size === "large" ? classes.large : classes.default,
shadow && classes.shadow,
className,
];
return (
<button {...props} className={buttonClasses.join(" ")}>
{children}
</button>
);
};
export default Button;
Variable Klassen
Das Ganze funktioniert so lange, bis man variable Klassen in der Komponente benötigt. Ich habe mal als Beispiel eine neue Indexpage erstellt, mit zwei Texten, die durch eine Komponente getrennt werden:
import Divider from '../components/Divider';
const IndexPage = () => {
return (
<div>Das ist der Text oberhalb der neuen Komponente</div>
<Divider />
<div>Das ist der Text unterhalb der neuen Komponente</div>
);
};
export default IndexPage;
Die Divider Komponente sieht bei so aus, diesmal zur Abwechslung mit Typescript:
const style = {
base: 'h-0 border-t-2'
}
type DividerProps = {
className?: string,
space?: number,
color?: string
}
const Divider = ({
className,
space = 1,
color = 'transparent'
}: DividerProps) => {
const classes = [
style.base,
`my-${space + 1} md:my-${space}`,
`border-${color}`,
className
].join(' ')
return <div className={classes} />
}
export default Divider
Ich habe hier eine Komponente erstellt, die einen Abstand zwischen zwei Elementen erzeugt. Dazu habe ich eine space
Variable eingeführt, die den Abstand angibt. Wenn ich den Divider jedoch ungefähr so in der IndexPage nutze:
[...]
<Divider space={4} />
[...]
bleibt der Abstand gleich, obwohl ich ihn auf 4 gesetzt habe. Und der Rahmen ist nicht transparent. Das liegt daran, dass PurgeCSS die Klassen my-4
und border-transparent
bereits gepurged hat. Die Dokumentation von Tailwind empfiehlt auch im Normalfall immer die Klasse als Ganzes zu nutzen. Nun gibt es zwei Möglichkeiten das zu beheben (natürlich könnte man auch einfach reines CSS oder SCSS nutzen, aber darum geht es hier nicht).
Kommentare
In eine der Dateien schreibt man in einen Kommentarbereich die Klassen, die nicht gepurged werden sollen. Das sieht dann so aus:
const style = {
base: 'h-0 border-t-2'
}
type DividerProps = {
className?: string,
space?: number,
color?: string
}
const Divider = ({
className,
space = 1,
color = 'transparent'
}: DividerProps) => {
const classes = [
style.base,
`my-${space + 1} md:my-${space}`,
`border-${color}`,
className
].join(' ')
return <div className={classes} />
}
export default Divider
// my-1 md:my-1
// my-2 md:my-2
// my-3 md:my-3
// my-4 md:my-4
// my-5 md:my-5
// my-6 md:my-6
// my-7 md:my-7
// my-8 md:my-8
// my-9 md:my-9
// my-10 md:my-10
// my-11 md:my-11
// my-12 md:my-12
// border-transparent
// border-primary
// border-secondary
Diese Klassen werden nun nicht mehr gelöscht.
Safelist
Die andere Lösung ist, die Klassen in der tailwind.config.js
Datei zu definieren. Dazu geben wir eine Safelist an:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {
colors: {
primary: "#1e89e7",
},
},
},
safelist: [
"border-transparent",
"border-primary",
{
pattern: /my-[0-9]+/,
},
],
plugins: [],
};
Die Reguläre Expression in der Safelist sagt, dass alle Klassen, die mit my-
beginnen und dann eine Zahl (eine oder mehrere Ziffern) folgt, nicht gelöscht werden sollen.
Beides sind natürlich nicht die schönsten Lösungen, insbesondere weil man auch Klassen reinnimmt, die man vielleicht nie nutzt, aber es funktioniert.
Fazit
In diesem Artikel habe ich eine Methode gezeigt, mit der ein Designsystem in Tailwind CSS nutzen kann ohne auf externe Plugins oder die vom Tailwind CSS Entwickler unbeliebten @apply Anweisungen zurückgreifen zu müssen. Ich hoffe, dass dieser Artikel hilfreich war und freue mich über Feedback.