npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

tiwi

v3.1.0

Published

React library to create components with Tailwind styles baked in.

Downloads

3,261

Readme

tiwi · GitHub license npm version

Tiwi is a React library that makes it easy to create components with Tailwind styles baked in. This makes it straightforward to preserve the separation of concern between structure and style, similar to styled-components. It also comes with a powerful variants system for more advanced use cases.

Tiwi was made to power BAQ.

// 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 $isUrgent={isUrgent}>
  <CardLink href={url}>Open in new window</CardLink>
</Card>;

Highlights

✅ Works with React on the web, SSR, and React Native. ✅ Full TypeScript compatibility. ✅ Extend existing components. ✅ Flexible variants system.

Table of contents

Getting started

1. Tailwind

Install and configure TailwindCSS in your project.

2. Tiwi

npm install tiwi

3. VSCode

For the best experience, install the official Tailwind CSS Intellisense extension.

The following configuration will allow the extension to work with Tiwi:

// As-you-type suggestions inside template strings.
"editor.quickSuggestions": {"strings": "on"},

// Handles `tiwi`…`` and `tiwi.div`…`` via the extension's own parser.
"tailwindCSS.classFunctions": ["tiwi", "tiwi\\.[a-z-]+"],

// classFunctions only fires when the tag name is immediately followed by a
// backtick, so call parens and generics break it. This covers exactly those
// shapes — `tiwi(Base)`, `tiwi.div<V>`, `tiwi(Base)<V>` — and captures the
// whole template so `${{…}}` variant blocks are included too.
"tailwindCSS.experimental.classRegex": [
  [
    "tiwi(?:\\.[\\w-]+)?(?:\\([^)]*\\)|<)[^`]*(`[\\s\\S]*?`);",
    "(?:[\"'`]|}}(?!}))([^\"'`$}]*)[\"'`$]"
  ]
]

Basic usage

Import Tiwi in your component file:

import tiwi from "tiwi";

You can now create Tiwi components:

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:

<Button />

// Renders as:
// <button class="rounded bg-blue-300" />

Classes can still be overridden inline:

<Button className="bg-red-300" />

// Renders as:
// <button class="rounded bg-red-300" />

Other props work as expected:

<Button type="submit">Submit</Button>

// Renders as:
// <button class="rounded bg-blue-300" type="submit">Submit</button>

Tiwi components can be further extended:

const BigButton = tiwi(Button)`
  text-lg
`;

// Renders as:
// <button class="rounded bg-blue-300 text-lg" />

When extending, styles can be overwritten:

const RedButton = tiwi(Button)`
  bg-red-300
`;

// Renders as:
// <button class="rounded bg-red-300" />

Any component with a className prop can be styled:

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.

Variant groups expose a prefixed styling prop directly on the component:

const SizeButton = tiwi.button`
  m-1
  p-2
  text-normal

  ${{
    size: {
      small: `p-3 text-sm`,
      large: `p-5 text-xl`,
    },
  }}
`;

The group name becomes a $-prefixed prop, and its values are inferred from the definition:

<SizeButton />;

// Renders as:
// <button class="m-1 p-2 text-normal" />

<SizeButton $size="large" />;

// Renders as:
// <button class="m-1 p-5 text-xl" />

String variants similarly expose a boolean styling prop:

const Button = tiwi.button`
  bg-blue-500

  ${{
    isDisabled: `bg-neutral-200`,
  }}
`;

<Button $isDisabled />;

Prefixed variant props are used only by Tiwi and are never forwarded to the DOM element or wrapped component. This keeps styling separate from behavioral props:

<Button disabled={isDisabled} $isDisabled={isDisabled} />

Multiple variant groups can be declared together:

const FlexButton = tiwi.button`
  p-2
  text-normal
  bg-blue-300

  ${{
    size: {
      medium: `p-3 text-lg`,
      large: `p-5 text-xl`,
    },
    intent: {
      primary: `bg-green-300`,
      critical: `bg-red-300`,
    },
  }}
`;

<FlexButton $size="medium" $intent="critical" />;

// Renders as:
// <button class="p-3 text-lg bg-red-300" />

Defaults can be declared explicitly with $defaultVariants. Default keys and values are checked against the other variants in the same interpolation:

const Button = tiwi.button`
  rounded

  ${{
    $defaultVariants: {
      size: "small",
      intent: "primary",
      isDisabled: false,
    },
    size: {
      small: `p-2 text-sm`,
      large: `p-4 text-lg`,
    },
    intent: {
      primary: `bg-blue-500 text-white`,
      secondary: `bg-neutral-100 text-neutral-900`,
    },
    isDisabled: `cursor-not-allowed opacity-50`,
  }}
`;

<Button />; // small + primary
<Button $size="large" />; // large + primary
<Button $size={null} />; // no size variant + primary

An omitted or undefined prop uses its default. An explicit prop overrides it, while null disables the variant entirely. For boolean variants, false and null both disable a true default. Defaults never create or forward synthetic props to wrapped components.

If a variant receives defaults in multiple interpolation blocks, the latest default is used component-wide. Nested Tiwi components keep their own defaults; only a value explicitly passed by the caller is forwarded through the component layers.

When multiple active variants set the same Tailwind property, the variant declared latest in the template wins:

const Button = tiwi.button`
  p-2

  ${{
    size: {
      large: `p-4 text-lg`,
    },
    density: {
      comfortable: `p-6 leading-loose`,
    },
  }}
`;

<Button $size="large" $density="comfortable" />;
<Button $density="comfortable" $size="large" />;

// Both render with:
// p-6 text-lg leading-loose

density is declared after size, so its p-6 replaces p-4. The order of the JSX props does not affect priority. The same declaration-order rule applies to legacy variant arrays and maps. An inline className is applied last and can override both base and variant styles.

Extended Tiwi components follow the same rule: base and extension classes are combined, with conflicting classes from the extension applied later.

The original variants prop is still useful when there is a single string union that already describes the component's styles. Provide that union as the generic parameter, then pass its value straight through:

type Size = "small" | "medium" | "large";

const LegacySizeButton = tiwi.button<Size>`
  p-2
  text-sm

  ${{
    medium: `p-3 text-lg`,
    large: `p-5 text-xl`,
  }}
`;

const MyComponent: FC<{size?: Size}> = props => {
  return <LegacySizeButton variants={props.size}>Continue</LegacySizeButton>;
};

variants also accepts an array or boolean map when older code needs to combine several flat variants. When a direct boolean prop and variants address the same variant, the direct prop takes precedence.

TypeScript

Tiwi is fully compatible with TypeScript and both props and variants are strongly typed automatically.

Grouped and boolean props are inferred directly from the declaration:

const Button = tiwi.button`
  ${{
    isDisabled: `opacity-50`,
    size: {
      small: `p-2`,
      large: `p-4`,
    },
  }}
`;

<Button $isDisabled $size="small" />;

The generated direct props can be extracted for wrappers and other component types:

import type {VariantPropsOf} from "tiwi";

type ButtonVariants = VariantPropsOf<typeof Button>;
// {$isDisabled?: boolean | null; $size?: "small" | "large" | null}

For components using the flat variants API, VariantsOf extracts the string union instead:

import type {VariantsOf} from "tiwi";

type Size = VariantsOf<typeof LegacySizeButton>;
// "small" | "medium" | "large"

React Native

Tiwi is compatible with React Native through Uniwind, which adds Tailwind className support to React Native components.

On React Native, import Tiwi from tiwi/native. It exposes the same API minus the DOM intrinsic elements (tiwi.div, tiwi.span, …), which don't exist on native:

import {View, Text} from "react-native";
import tiwi from "tiwi/native";

const Avatar = tiwi(View)`
  rounded-full

  ${{
    size: {
      small: `size-8`,
      large: `size-12`,
    },
  }}
`;

const Title = tiwi(Text)`
  text-neutral-900
  dark:text-white
`;

<Avatar $size="large" />;

Core React Native components (View, Text, etc.) work out of the box. To style a third-party component that doesn't natively support className, wrap it with Uniwind's withUniwind first:

import {withUniwind} from "uniwind";
import {SafeAreaView} from "react-native-safe-area-context";
import tiwi from "tiwi/native";

const Screen = tiwi(withUniwind(SafeAreaView))`
  flex-1
  bg-white
`;

Full example

// 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`
  flex
  flex-row

  rounded
  p-2
  gap-1

  bg-neutral-200

  ${{
    variant: {
      normal: ``,
      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 $variant={variant}>
      <Icon>{icon}</Icon>
      <Text>{children}</Text>
    </Layout>
  );
};

Acknowledgements

This library was inspired by Tailwind-Styled-Component and borrows some of its ideas. It also heavily relies on tailwind-merge for the underlying class manipulation and shares the same limitations.

License

Tiwi is MIT licensed.