tiwi
Version:
React library to create components with Tailwind styles baked in.
427 lines (319 loc) • 8.22 kB
Markdown
# tiwi · [](https://github.com/baqhub/tiwi/blob/main/LICENSE) [](https://www.npmjs.com/package/tiwi)
Tiwi is a [React](https://react.dev/) library that makes it easy to create components with [Tailwind](https://tailwindcss.com/) styles baked in. This makes it straightforward to preserve the separation of concern between structure and style, similar to [styled-components](https://styled-components.com/). It also comes with a powerful variants system for more advanced use cases.
Tiwi was made to power [BAQ](https://baq.dev).
```tsx
// Don't write this:
<div className=`p-2 rounded bg-neutral-100 hover:bg-white shadow-md ${isUrgent ? "shadow-red-200" : "shadow-neutral-200"}`>
<a className="text-md text-green-400 font-semibold" href={url}>Open in new window</a>
</div>;
//
// Write this instead:
//
const Card = tiwi.div`
p-2
bg-neutral-100
hover:bg-white
shadow-md
shadow-neutral-200
${{
isUrgent: `shadow-red-200`,
}}
`;
const CardLink = tiwi.a`
text-md
text-green-400
font-semibold
`;
<Card variants={{isUrgent}}>
<CardLink href={url}>Open in new window</CardLink>
</Card>;
```
### Highlights
✅ Works with React on the web, SSR, and React Native.<br>
✅ Full TypeScript compatibility.<br>
✅ Extend existing components.<br>
✅ Flexible variants system.
### Table of contents
- [Getting started](#getting-started)
- [Basic usage](#basic-usage)
- [Variants](#variants)
- [TypeScript](#typescript)
- [React Native](#react-native)
- [Full example](#full-example)
- [Acknowledgements](#acknowledgements)
- [License](#license)
## Getting started
### 1. Tailwind
[Install and configure](https://tailwindcss.com/docs/installation) TailwindCSS in your project.
### 2. Tiwi
```bash
npm install tiwi
```
### 3. VSCode
For the best experience, install the official [Tailwind CSS Intellisense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) extension.
The following configuration will allow the extension to work with Tiwi:
```jsonc
"editor.quickSuggestions": {
"strings": "on" // As-you-type suggestions within strings.
},
"tailwindCSS.experimental.classRegex": [
[
"(?:tiwi|tw)[.(][^`]+([^;]*)",
"[\"'`\\}]([^\"'`\\$]*)[\"'`\\$]"
]
]
```
## Basic usage
Import Tiwi in your component file:
```tsx
import tiwi from "tiwi";
```
You can now create Tiwi components:
```tsx
const Header = tiwi.h1`
text-3xl
text-blue-600
`;
const Button = tiwi.button`
rounded
bg-blue-300
`;
```
These can be used like any other component:
```tsx
<Button />
// Renders as:
// <button class="rounded bg-blue-300" />
```
Classes can still be overridden inline:
```tsx
<Button className="bg-red-300" />
// Renders as:
// <button class="rounded bg-red-300" />
```
Other props work as expected:
```tsx
<Button type="submit">Submit</Button>
// Renders as:
// <button class="rounded bg-blue-300" type="submit">Submit</button>
```
Tiwi components can be further extended:
```tsx
const BigButton = tiwi(Button)`
text-lg
`;
// Renders as:
// <button class="rounded bg-blue-300 text-lg" />
```
When extending, styles can be overwritten:
```tsx
const RedButton = tiwi(Button)`
bg-red-300
`;
// Renders as:
// <button class="rounded bg-red-300" />
```
Any component with a `className` prop can be styled:
```tsx
const SubmitButton: FC<{className?: string}> = props => {
return (
<button className={props.className} type="submit">
Submit
</button>
);
};
const RedSubmitButton = tiwi(SubmitButton)`
bg-red-300
`;
// Renders as:
// <button class="bg-red-300" type="submit">Submit</button>
```
## Variants
What makes Tiwi so powerful is the built-in support for variants. It enables the style of a component to be changed along multiple dimensions without creating every permutation separately.
Here's a simple variant to support multiple sizes:
```tsx
const SizeButton = tiwi.button`
m-1
p-2
text-normal
${{
medium: `
p-3
text-lg
`,
large: `
p-5
text-xl
`,
}}
`;
```
The desired variants can be provided in different ways:
```tsx
<SizeButton />;
// Renders as:
// <button class="m-1 p-2 text-normal" />
<SizeButton variants="medium" />;
<SizeButton variants={["medium"]} />;
<SizeButton variants={{medium: true}} />;
// All render as:
// <button class="m-1 p-3 text-lg" />
```
If multiple variants overlap, the last one to be declared wins:
```tsx
<SizeButton variants={["medium", "large"]} />;
<SizeButton variants={["large", "medium"]} />;
<SizeButton variants={{medium: true, large: true}} />;
// All render as:
// <button class="m-1 p-5 text-xl" />
```
Variants can be specified along separate dimensions:
```tsx
const FlexButton = tiwi.button`
p-2
text-normal
bg-blue-300
${{
medium: `
p-3
text-lg
`,
large: `
p-5
text-xl
`,
}}
${{
primary: `
bg-green-300
`,
critical: `
bg-red-300
`,
}}
`;
```
```tsx
<FlexButton variants={["medium", "critical"]} />;
<FlexButton variants={{medium: true, critical: true}} />;
// Both render as:
// <button class="p-3 text-lg bg-red-300" />
```
Variants can directly match some of the component's props:
```tsx
const Button = tiwi.button`
bg-blue-500
${{
isDisabled: `bg-neutral-200`,
}}
`;
const MyComponent: FC<{isDisabled?: boolean}> = props => {
return <Button variants={props}>Continue</Button>;
};
```
Base style and variants can be declared in any order:
```tsx
const FlexButton = tiwi.button`
text-normal
${{
medium: `text-lg`,
large: `text-xl`,
}}
bg-blue-300
${{
primary: `bg-green-300`,
critical: `bg-red-300`,
}}
`;
```
## TypeScript
Tiwi is fully compatible with TypeScript and both props and variants are strongly typed automatically.
On top of that, an explicit variant type can be provided:
```tsx
type Size = "small" | "medium";
const SizeButton = tiwi.button<Size>`
text-normal
${{medium: `text-medium`}}
`;
const MyComponent: FC<{size?: Size}> = props => {
return <SizeButton variants={props.size}>Press me!</SizeButton>;
};
```
The opposite (extracting the variants type) is also possible:
```tsx
import tiwi, {VariantsOf} from "tiwi";
const SizeButton = tiwi.button`
text-normal
${{
medium: `text-medium`,
large: `text-large`,
}}
`;
type Variants = VariantsOf<typeof SizeButton>; // "medium" | "large".
```
## React Native
Tiwi is compatible with React Native. It requires [NativeWind](https://www.nativewind.dev/) to be installed.
React Native elements can then be styled in the same way:
```tsx
import {View, Text} from "react-native";
import tiwi from "tiwi";
const Avatar = tiwi(View)`
size-10
rounded-full
`;
const Title = tiwi(Text)`
text-neutral-900
dark:text-white
`;
```
## Full example
```tsx
// tooltip.tsx
import {FC, PropsWithChildren, ReactNode} from "react";
import tiwi from "tiwi";
//
// Props.
//
type TooltipVariant = "normal" | "important";
interface TooltipProps extends PropsWithChildren {
variant?: TooltipVariant;
icon: ReactNode;
}
//
// Style.
//
const Layout = tiwi.div<TooltipVariant>`
flex
flex-row
rounded
p-2
gap-1
bg-neutral-200
${{
important: `bg-red-200`,
}}
`;
const Icon = tiwi.div`
size-5
`;
const Text = tiwi.div`
text-neutral-900
font-medium
`;
//
// Component.
//
export const Tooltip: FC<TooltipProps> = props => {
const {variant, icon, children} = props;
return (
<Layout variants={variant}>
<Icon>{icon}</Icon>
<Text>{children}</Text>
</Layout>
);
};
```
## Acknowledgements
This library was inspired by [Tailwind-Styled-Component](https://github.com/MathiasGilson/Tailwind-Styled-Component/tree/master) and borrows some of its ideas. It also heavily relies on [tailwind-merge](https://github.com/dcastil/tailwind-merge) for the underlying class manipulation and shares the same [limitations](https://github.com/dcastil/tailwind-merge/blob/v2.5.4/docs/limitations.md).
## License
Tiwi is [MIT licensed](https://github.com/baqhub/tiwi/blob/main/LICENSE).