Tailwind CSS Framework - Komponenten

In diesem Artikel zeige ich, wie man dem Tailwind CSS Framework wiederverwendbare Komponenten mit variablen CSS Klassen erstellen kann.

Tailwind CSS Framework - Komponenten-heroimage

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;

index.jsx

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;

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;

Variante

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;

Mit Klassennamen

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;

Size

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.


Diese Website verwendet Cookies. Diese sind notwendig, um die Funktionalität der Website zu gewährleisten. Weitere Informationen finden Sie in der Datenschutzerklärung