Deployn

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