Giter Site home page Giter Site logo

tw-in-js / twind Goto Github PK

View Code? Open in Web Editor NEW
3.7K 18.0 95.0 43.55 MB

The smallest, fastest, most feature complete Tailwind-in-JS solution in existence.

Home Page: https://twind.style

License: MIT License

JavaScript 59.97% TypeScript 32.52% HTML 0.05% Shell 0.01% Svelte 5.66% Smarty 1.81%
tailwindcss tailwind-in-js css-in-js tailwind css

twind's Introduction

The smallest, fastest, most feature complete Tailwind-in-JS solution in existence


MIT License Latest Release Documentation Github Discord CI Coverage Status

Twind is a small compiler that converts utility classes into CSS at runtime. The goal of this project is to unify the flexibility of CSS-in-JS with the carefully considered constraints of the Tailwind API.

Utility-first CSS without any build step right in the browser or any other environment like Node.js, deno, workers, ...


Nov 18, 2022: Twind v1 is now in stable release!

Check out the Migration Guide to upgrade or go to the v0.16 branch.


โœจ Features

โšก๏ธ No build step

Get all the benefits of Tailwind without the need for Tailwind, PostCSS, configuration, purging, or autoprefixing.

๐Ÿš€ Framework agnostic

If your app uses HTML and JavaScript, it should work with Twind. This goes for server-rendered apps too.

๐Ÿ˜Ž One low fixed cost

Twind ships the compiler, not the CSS. This means unlimited styles and variants for one low fixed cost.

Other features
  • ๐ŸŒŽ No bundler required: Usable via CDN
  • ๐ŸŽจ Seamless integration with Tailwind
  • ๐Ÿค Feature parity with Tailwind v3
  • ๐ŸŽฏ Extended variants, rules, and syntax
  • ๐Ÿš“ Escape hatch for arbitrary CSS
  • ๐Ÿค– Built in support for conditional rule combining
  • ๐Ÿง Improved readability with multiline styles and comments
  • โ„๏ธ Optional hashing of class names ensuring no conflicts
  • ๐Ÿ”ฉ Flexible: configurable theme, rules and variants
  • ๐Ÿ”Œ Language extension via presets
  • ๐ŸŽฉ No runtime overhead with static extraction
  • ๐Ÿš… Faster than most CSS-in-JS libraries
  • โšก Fully tree shakeable: Only take what you want
  • ๐Ÿฆพ Type Strong: Written in Typescript
  • and more!

๐Ÿ“– Documentation

The full documentation is available at twind.style.

๐Ÿ’ฌ Community

For help, discussion about best practices, or any other conversation that would benefit from being searchable use Github Discussions.

To ask questions and discuss with other Twind users in real time use Discord Chat.

๐Ÿงฑ Contribute

See the Contributing Guide for information on how to contribute to this project.

๐ŸŒธ Credits

๐Ÿ’ก Inspiration

It would be untrue to suggest that the design here is totally original. Other than the founders' initial attempts at implementing such a module (oceanwind and beamwind) we are truly standing on the shoulders of giants.

  • Tailwind CSS: created a wonderfully thought out API on which the compiler's grammar was defined.
  • styled-components: implemented and popularized the advantages of doing CSS-in-JS.
  • htm: a JSX compiler that proved there is merit in doing runtime compilation of DSLs like JSX.
  • goober: an impossibly small yet efficient CSS-in-JS implementation that defines critical module features.
  • otion: the first CSS-in-JS solution specifically oriented around handling CSS in an atomic fashion.
  • clsx: a tiny utility for constructing class name strings conditionally.
  • style-vendorizer: essential CSS prefixing helpers in less than 1KB of JavaScript.
  • UnoCSS: for the configuration syntax.
  • CSSType: providing autocompletion and type checking for CSS properties and values.

๐Ÿค Contributors

Thank you to all the people who have already contributed to twind!

๐Ÿ™๐Ÿพ Sponsors

This project is kindly sponsored by Kenoxa GmbH who support @sastan to maintain this project as part of their open-source engagement.

COPILOT TRAVEL is partnering with @sastan to keep twind aligned with the latest Tailwind CSS releases.

Thank you to all our sponsors!

Sponsors

Please ask your company to also support this open source project by becoming a sponsor on opencollective or GitHub.

โš–๏ธ License

The MIT license governs your use of Twind.

twind's People

Contributors

danielduel avatar danielweck avatar github-actions[bot] avatar ignusg avatar itsmapleleaf avatar javascriptjedi avatar nexxeln avatar nonissue avatar pi0 avatar renhiyama avatar rschristian avatar sastan avatar sebilasse avatar sergeizheleznov avatar thecmdrunner avatar tranquilmarmot avatar wommy avatar wzulfikar avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

twind's Issues

Parse classNames to inline styles.

It would be nice if we could use the server side rendering part, to style emails with this library.

The thing is, not all mail clients know how to handle style tags. The recommended approach is still to use inline styles (<div style="color: red">).

I do realize that it's not possible to support everything with that approach, but even partial support could be of immense value when creating mail templates.

Things that are unsupported (like media queries) could still go to the head style sheet for those clients that do support it.

It would also mean that css variables should be resolved.

Thoughts?

Support Grid customization

Hello,

As far as I see there is no support to customize any of the grid properties (e.g. gridTemplateColumns). I tried to search in issues and discussions but didn't find any information. Is there any plans to add such support ?

Thanks

[Question] Is there a way to generate all tailwind classes at runtime ?

As of now only used css classes are generated at runtime (which is the expected behavior for production).
However, during development, it is really handy to have all css classes available in the devtools. Like using tailwind without purgecss.

image

Is there a way to do that with the current version?

Concurrent async SSR

Hi all, fantastic work on twind!

I was looking into integrating this into an experiment of mine that brings full async SSR but noticed that twind's SSR docs appear to assume that only one concurrent render operation can be in effect at once. Are there any tricks I'm missing that would allow me to potentially:

  1. Start rendering page A
  2. Start rendering page B
  3. Start rendering page C
  4. Finish rendering page A and generate static stylesheet
  5. Finish rendering page C and generate static stylesheet
  6. Finish rendering page B and generate static stylesheet

The potential for interleaving and out-of-order completion has me a bit worried.

Discussion: Worth escaping user-provided values in generated markup

Description

When used in an SSR context, getStyleTag is designed to produce a <style> tag suitable for embedding in generated HTML so that the initial payload has all critical CSS inlined.

The act of embedding anything into server-generated HTML introduces an XSS risk. That risk is proportional to the likelihood that user-submitted content might find its way into the generated markup. In the general case, this risk seems quite low for a tool like twind.

That being said, I suspect some creative folks will realize that twind is an unusually effective way of rendering similar markup with different themes in the same page. They may also think that it would be interesting to allow users to create and share their own themes. At this point we're pretty far into the realm of the hypothetical and should think about balancing risk vs cost.

Below, I present some links where similar issues have been brought for other frameworks living in the same space as twind.

My conclusion is that on the balance of risk and cost, a strategic replacement of </ (and maybe html comments?) would go a long way in closing the XSS window at a very small cost.

Research

  1. 2018 to treat escaping as the problem of the implementer of styled-components: styled-components/styled-components#1828.
  2. 2020: Proposal to sanitize interpolated strings in styled-components: styled-components/styled-components#1105 (comment)
  3. 2020: Discussion of XSS risk in SSR styles in jss: cssinjs/jss#1265

Tools

When injecting JavaScript of JSON into HTML, my go-to solution is jsesc.

API consideration: return `tw` from `setup` function

So one issue I notice with the way the API works, is that you have to manually ensure tw is only called after setup. Integrating twind in my Next.js project and having followed the examples, even then, I'm still getting some "late setup call" errors, which I think is a sign that this constraint can't be reliably followed

Suggestion: when needing a config override, the user should create their own tw file which includes the setup call. Setup returns the tw function, which is then imported throughout the app for styling. Logically, this ensures that setup can't be called after tw. To clarify:

// tw.ts
import { setup } from "twind"

export const tw = setup({
	theme: { colors: { palevioletred: "palevioletred" } },
})

// Button.tsx
import { tw } from "./tw"

export const Button = () => (
	<button type="button" className={tw`p-3 rounded bg-blue-500 text-white`}>
		hi
	</button>
)

With the current API, it's true that one could also reexport tw from the tw.ts file. My suggestion is to make this API change to possibly enforce using the returned tw function, and/or document that usage

Scoped preflight for gradual migration

I'm adding twind to an existing React project with its own CSS (via emotion). I noticed that twind styles are well isolated to the components that use them, but once the component unmounts, some of the base styles (resets, I think) linger on the page.

Is there a recommended approach to cleanly removing these when the component is unmounted?

twind/shim: support for static extraction a.k.a. server side rendering

The shim currently works only in browser environments. To support static extraction it must detect in which environment it runs and parse/analyze the generate html to inject the needed styles.

As a first use case we will try to support wmr prerender as it return a string of html which should be analyzable.

  • allow the shim to work in non-browser environments
  • analyze the generated html
  • generate the required styles
  • replace the class names (expand grouping, replace with hashed class names)

Proposal: add Next.js to docs/examples

Are you open for a PR to add Next to https://github.com/tw-in-js/twind/blob/main/docs/examples.md ?

It took me a long time to figure out how to get this working in Next.js. The reason I got confused was because most React examples use below double quoted examples like shown below:

    <main className="${tw`h-screen bg-purple-400 flex items-center justify-center`}">
      <h1 className="${tw`font-bold text(center 5xl white sm:gray-800 md:pink-700)`}">
        This is Twind!
      </h1>
    </main>

After removing the double quotes, things simply started working โค๏ธ

    <main className={tw`h-screen bg-purple-400 flex items-center justify-center`}>
      <h1 className={tw`font-bold text(center 5xl white sm:gray-800 md:pink-700)`}>
        This is Twind!
      </h1>
    </main>

How to delete internal `<style id="__twind">`?

Hi, thank you for this cool library!

I was working on a React app project, everything went fine until I wrote my own Tailwind configuration. The configuration I wrote down lost in the specificity battle (because of generated internal style).

My configuration:

const plugin = require('tailwindcss/plugin')

module.exports = {
    purge: [],
    darkMode: 'media', // or 'media' or 'class'
    theme: {
        extend: {
            fontFamily: {
                sans: [
                    '"Source Sans Pro"',
                ], 
....

Specificity:
image

I found the internal style in DevTools which this library generates. In the documentation I found that to use the twind function I didn't need the setup function. How can I just use the twind function to group Tailwind classes? Without any internal styles and my configuration works fine.

Thanks.

Tailwind VS Code intellisense doesn't work

Hey there, this is an interesting tool. I love that you don't have to mess around with the build step to use it. However, the tailwind intellisense doesn't work when using this library which is killer. I noticed the VS code extension says it will activate if "tailwind css is installed and a tailwind config exists". But obviously there's no reason to install tailwind if using twind. Any thoughts here or something I'm missing? Thanks!

`justify-self-start` not working, resulting class with empty rule

Please see this minimum example

<section class="grid grid-cols-3 gap-2 place-items-center h-screen w-screen">
  <div class="bg-pink-500 text-white justify-self-start">1</div>
  <div class="bg-pink-500 text-white">2</div>
  <div class="bg-pink-500 text-white">3</div>
  <div class="bg-pink-500 text-white">4</div>
  <div class="bg-pink-500 text-white">5</div>
  <div class="bg-pink-500 text-white">6</div>
</section>

Tailwind Play

Correct Result

image

But in twind, it's not working

Code Sandbox

image

image

Official Docs here

Static extraction causes specificity issue with new styles during runtime

Repo: https://github.com/itsMapleLeaf/twind-specificity-issue

image

Where the text says "this element should be translucent, but is not", that has bg-black bg-opacity-25 on it, but the bg opacity isn't applying due to the specificity of bg-black's default opacity.

Here's the code that reproduces this:

export default function Home() {
	const [isReady] = useTimeout(100)
	return (
		<>
			<Portal>
				<div className={tw`bg-black text-white`}>portal element</div>
			</Portal>
			{isReady() && (
				<div className={tw`bg-black bg-opacity-25 text-white`}>
					this should be translucent, but is not?
				</div>
			)}
		</>
	)
}

It happens only with the Portal on the first element, and a delayed render of the second. If anything about this changes, then the issue goes away. Very interesting and strange ๐Ÿค”

Plugin for CSS content property

It would be very helpful to have a content plugin (@twind/content) which allows to define CSS content property.

Why not in core? There is not a official Tailwindcss content specification. By not adding it to core we prevent future incompatibilities.

Prior Work


Some features described below depend on pseudo elements support (#103).

Usage as Directive

โ— Values must be adhere to CSS content property syntax.

tw`${content('"โœ…"')}`
// => .tw-xxxx { content: "โœ…" }

tw`before::${content('"โœ…"')}`
// => .tw-xxxx::before { content: "โœ…" }

tw`before::${content('attr(data-content)')}`
// => .tw-xxxx::before { content: attr(data-content) }

tw`after::${content('" (" attr(href) " )"')}`
// => .tw-xxxx::after { content: " (" attr(href) " )" }

tw`${content({ before: '"๐Ÿ’ก"', after: '"โœ…"' })}`
// => .tw-xxxx::before { content: "๐Ÿ’ก" }
// => .tw-xxxx::after { content: "โœ…" }

tw`${content({ before: 'attr(data-before)', after: 'attr(data-after)' })}`
// => .tw-xxxx::before { content: attr(data-before) }
// => .tw-xxxx::after { content: attr(data-after) }

Usage as Plugin

I see the following usage patterns which should be tried in order:

  1. content-<theme> with { theme: { content: { key: value } } }
tw`before::content-check` // with { theme: { content: { check: '"โœ…"' } } }
// => .before\:\:content-check::before { content: "โœ…"; }
  1. content-(known value)

List of known values:

  • open-quote
  • close-quote
  • no-open-quote
  • no-close-quote
  • normal
  • none
  • inherit
  • initial
  • unset
tw`before::content-open-quote`
// => .before\:\:content-open-quote::before { content: open-quote; }
  1. content[-(attr|data|counter|var)-value]
tw`content`
// => .content { content: attr(data-content); }

tw`before::content`
// => .before\:\:content::before { content: attr(data-content); }

tw`before::content-data-before`
// => .before\:\:content-data-before::before { content: attr(data-before); }

tw`before::content-attr-data-before`
// => .before\:\:content-attr-data-before::before { content: attr(data-before); }

tw`before::content-counter-chapter_counter`
// => .before\:\:content-attr-data-before::before { content: counter(chapter_counter); }

tw`before::content-var-before`
// => .before\:\:content-var-before-before::before { content: var(--before); }
  1. content-value using JSON.stringify
tw`before::content-โœ…`
// => .before\:\:content-โœ…::before { content: "โœ…"; }

Do you have any comments? Did I miss something?

/cc @tw-in-js/contributors

Module broken?

This is probably just an issue with Skypack, but it seems like there's an issue w/ the module being served. The demos all appear broken and have this error:
Uncaught TypeError: Cannot destructure property 'cssPropertyAlias' of '__commonjs_module0' as it is null.

CSS `content` property is dropped by twind/css directive

I havenโ€™t been able to get the examples in the twind/css docs to work as the content property doesnโ€™t seem to be processed by the CSS directive. Other properties (background in the example) are passed through and displayed when ::before or ::after content is set manually.

Repro [pen]

import { css } from "https://cdn.skypack.dev/twind/css";

const styles = css({
  "&::before": { background: "red", content: "๐Ÿ™" },
  "&::after": { background: "green", content: "๐Ÿ˜Š" }
});

document.body.innerHTML = `
  <style>
    /* override */
    .${styles}::after { content: "๐Ÿ‘‹" }
  </style>
  <p class="${styles}">Hi!</p>
`;

Actual render: image

Expected render: image

Extended color theme can't down to level three.

Please see this minimum example

In this Tailwind Play I created, the color theme can be extended down to three-level like this

Normal Tailwind

module.exports = {
  theme: {
    extend: {
      colors: {
        v2: {
          red: {
            500: '#DC2626',
          },
        },
      },
    },
  },
}
<div class="text-v2-red-500">Three level custom color</div>

Result:

image

However, in twind, it's not working, and typing yells also

Codesandbox

Result:

image

Preflight merging/extending strategy

I think it's reasonable to assume one might want to expand on or modify the default Tailwind preflight but native Tailwind doesn't currently provide a way to do this. So twind gets the luxury of doing it first! I'd like to open up a discussion about how that feature might look.

My first thought would be to possibly provide an extend key which could behave similarly to theme.extend.

preset configuration option

Currently new plugins are added via the plugins object which are merge with the builtin plugins:

setup({
  plugins: {
    'scroll-snap': () => { /* ... */ }
  },
})

Some modules (like typography eg prose) need to add several plugins and may add a new theme section.

We are thinking about supporting this using a new configuration property:

setup({
  presets: [
    function typographyPreset({ theme, plugins }) {
      return {
        theme: {
          typography: {
            textColor: theme("colors.gray.600"),
            linkColor: theme("colors.blue.600"),
          },
        },

        plugins: {
          lead: () => {
            /* ... */
          },
          prose: () => {
            /* ... */
          },
        },
      };
    },
  ],
});

A preset has access to the current constructed configuration via its first parameter and should return a new partial configuration which will be merged with the current configuration.

As an alternative we could allow setup to accept a function which is called with the defaults and provide a withPresets function (maybe as @tw-in-js/with-presets)

setup(
  withPresets(
    function typographyPreset({ theme, plugins }) {
      return {
        theme: {
          typography: {
            textColor: theme("colors.gray.600"),
            linkColor: theme("colors.blue.600"),
          },
        },

        plugins: {
          lead: () => {
            /* ... */
          },
          prose: () => {
            /* ... */
          },
        },
      };
    },
));

Using this withPresets this basic implementation which can be improved separately:

function withPresets(...presets) {
  return (config) => presets.reduce((config, preset) => ({ ...config, ...preset(config) }), config)
}

`tw(...)` vs `tw([...])` resulting different class

Please see this minimum example

import { tw } from "twind";

document.body.innerHTML = `
  <div class="${tw("bg-pink-50", "hover:text-pink-900", "p-3")}">
    Not array
  </div>

  <hr />

  <div class="${tw(["bg-pink-50", "hover:text-pink-900", "p-3"])}">
    Array
  </div>
`;

CodeSandbox

Screenshot

image

Everything behind hover: will be ignored when using tw(...)

Is there a way to obfuscate the Class Name?

Problem

To complicate the theft of components that have been made, I usually obfuscate the existing class names, so that the thief has difficulty refactoring the code he took on the original website.

But, this library is run at runtime, I am confused about how to obfuscate the existing class names. Do you have any suggestions?

My Expectation

Input

tw`text-gray-100 bg-green-200 relative`

Output

"a b c"
  • text-gray-100 has been obfuscated to a
  • bg-green-200 has been obfuscated to b
  • relative has been obfuscated to c

The obfuscating process above is only doing in the Production stage.

Thanks.

Find a npm package name

These are all available on npm:

  • gusty
  • trablovo
  • twish
  • twings
  • twilt
  • twind
  • twinjet
  • twiny
  • twizz
  • tawie
  • tawy
  • tawing
  • straalwind
  • venta
  • velado
  • vetra

The next ones are squatted. We could reach out to npm for these.

  • tw
  • twin
  • vento

Twind with Preact WMR

Hi,this is an amazingย tool, thankย you for you work. I'll try to use twind inside the new Preact WMR ( https://github.com/preactjs/wmr ) but with the only npm install twind and tailwindcssย and the following use:

import { useState } from 'preact/hooks';
import { tw } from 'twind'

export default function Home() { 
 return ( 
<> 
<p>Hi!</p> 
<h1 className={tw`font-bold text(center 5xl white sm:gray-800 md:pink-700)`}>This is Twind!</h1> 
</> );
}

doesn't work,ย i have a white page and no error as output of npm run start. Is it possible to use with Preact WMR?
Thank you.

Docs: Broken demo

I'd submit a pr for this but it involves the live demo for observe so it probably needs to be updated there.

It works again when
import { observe } from 'twind/observe'
is changed to
import { observe } from 'https://cdn.skypack.dev/twind/observe'

Documentation Updates

I have re-structured the documentation to have a good starting point to provide a better "on-boarding": https://twind.dev/docs/

I have splitted the documentation in to two parts:

  1. Handbook: more like a tutorial and some "How To" โ€“ sourced from the docs/ folder (see docs/pages.json)
  2. API: reference describing the machinery โ€“ sourced from README.md in src/ (for example src/shim/README.md)

The website is currently updated on every push to main. Later I would like to only update the website on releases to have a stable documentation for a release that is not mixed with upcoming features.

What needs to be done?

  • docs/getting-started/styling-with-twind.md: use a tutorial approach โ€“ other parts of "Getting Started" may need some review to
  • Empty Pages
    • docs/getting-started/quicksheet.md
    • docs/getting-started/best-practices.md
    • docs/recipes/README.md
    • docs/recipes/use-with/vue.md
    • docs/migrate-from/tailwindcss.md
    • docs/migrate-from/twin-macro.md
    • docs/faqs/README.md
    • docs/release-notes.md
  • create a theme for typedoc using twind

I'm sure there is a lot more to improve. I hope that some native speakers may pick some of the tasks and provide PRs for these.

You can build the website locally using: yarn typedoc && open website/docs/index.html

/cc @tw-in-js/contributors

Inline plugin global styles

So since the current approach to declaring global styles seems to be best accomplished through preflight, it would be nice to provide some kind of merging strategy for common selector keys as to avoid this sort of code...

preflight: ({ html, ...preflight }, { theme }) => ({
  ...preflight,
  html: {
    color: theme('colors.gray.900'),
    ...html,
  },
})

Otherwise, I think there should be some discussion about ways to declare global styles.

Regarding this new (:global)[https://github.com/tw-in-js/twind/blob/main/docs/plugins.md#inject-global-styles] property, is it at all possible to declare global styles from within inline plugins? That would be a nifty way to allow declaration of global styles from virtually anywhere in your app.

Looking a way to parse classes to style string

Hi Guys.
I'm working with twind and looks pretty good, but I have a question.

Exist a way to parse to styles string with classes reference?

I've used getStyleTag but it returns a style string with its elements and I would to know if it could set classes instead.

Thanks for your job!

More rapid, intuitive use in NodeJS context

for lazy Node devs this would be ideal:

npm install twind
import { tw, getStyleTag } from 'twind' 
let html = `
  <div class="${tw`text-xl`} block px-20 text-green-100">
    can optionally use the tw function or vanilla Tailwind to sprinkle classes 
  </div>
`
let styleTag = getStyleTag(html)
//profit

here the tw function needs no dom or sheet; operating directly on the string provided. The getStyleTag function can do its thing to get only the styles used whether vanilla classes or those created with said tw function; and without using a shim.

Those other features obviously useful for more advanced use cases but here in the simplest possible server side use case it could become obvious how to get what we need without further ritual or mental energy/documentation necessary to understand other concepts.

Support Tailwindcss Plugins

Tailwindcss Plugin Documentation

Benefits

Supporting plain tailwindcss plugins would allow users to re-use existing plugins and prevent introducing a new plugin API.

Plugin API

addUtilities(), for registering new utility styles

plugin(function({ addUtilities }) {
  const newUtilities = {
    '.skew-10deg': {
      transform: 'skewY(-10deg)',
    },
    '.skew-15deg': {
      transform: 'skewY(-15deg)',
    },
  }

  addUtilities(newUtilities)
})

We can convert the CSS-in-JS object using twind/css. For each top-level key extract the plugin name and register it's definiton as a twind plugin.

addUtilities() accepts several options. We should discuss if support for these is required for an initial release.

addUtilities(newUtilities, {
  respectPrefix: false,
  respectImportant: false,
  variants: ['responsive', 'hover']
})

To be fully compatible we should support prefix and important configuration settings.

addComponents(), for registering new component styles

Works just like addUtilities() but adds the generated classes into the component layer. We could increase the presedence for these to simulate a component layer.

addBase(), for registering new base styles

This should add the generated classes to the preflight. They should not be hashed or prefixed.

addVariant(), for registering custom variants

Allows you to register your own custom variants that can be used just like the built-in hover, focus, active, etc. variants.

plugin(function({ addVariant, e }) {
  addVariant('disabled', ({ modifySelectors, separator }) => {
    modifySelectors(({ className }) => {
      return `.${e(`disabled${separator}${className}`)}:disabled`
    })
  })
})

e(), for escaping strings meant to be used in class names

Already implemented in util.ts as escape.

prefix(), for manually applying the user's configured prefix to parts of a selector

The prefix function will prefix all classes in a selector and ignore non-classes, so it's totally safe to pass complex selectors like this one

prefix('.btn-blue .w-1\/4 > h1.text-xl + a .bar')
// => '.tw-btn-blue .tw-w-1\/4 > h1.tw-text-xl + a .tw-bar'

theme(), for looking up values in the user's theme configuration

Allow to access the theme. Already implemented within the theme resolver.

variants(), for looking up values in the user's variants configuration

This may be no-op as twind supports all variants for every directive.

config(), for looking up values in the user's Tailwind configuration

Provides access to the default configuration.

postcss, for doing low-level manipulation with PostCSS directly

This may never be available as twind is not using postcss. On accessing the property an error should be thrown.

Registering Plugins

setup must support an array of functions:

setup({
  plugins: [
    plugin(function({ addUtilities, addComponents, e, prefix, config }) {
      // Add your custom styles here
    }),
  ]
})

Challenges

Plugins depend on 'tailwindcss/plugin'

This could be solved setting up the bundler to alias tailwindcss/plugin to twind/plugin. Preact has a good guide how to do that.

Most plugins are written in CJS

Importing plugins may work in bundler environment. But if used directly in the browser this may not work.

Many plugins use lodash (bundle size)

This is not really a challenge but points more to how using plugins could increase the bundle size.

CSS objects as tw parameter and interpolations value

Currently we interpret objects as nested rules (see docs/usage):

tw({
  sm: 'w-1/2',
  md: 'w-1/3',
})
// => sm:w-1/2 md:w-1/3

tw`sm:${{ underline: true }}`
// => sm:underline

I wonder if there is really a use case for this kind of rule definitions?

The above examples could be written using the preferred (performance) template literal syntax:

tw`sm:w-1/2 md:w-1/3`
tw`w(sm:1/2 md:1/3)`
// => sm:w-1/2 md:w-1/3

tw`sm:${true && 'underline'}`
tw`sm:${[true && 'underline']}`
// => sm:underline

To have custom css you currently need to use inline plugins (a function returning a css object):

tw(() => ({
  '&::before': { content: '๐Ÿ™' }
  '&::after': { content: '๐Ÿ˜Š' }
}))
// => sm:hover:tw-xxxxx

I propose that we drop the nested rules object in favor of CSS objects:

tw`
  sm:hover:${{
    '&::before': { content: '๐Ÿ™' }
    '&::after': { content: '๐Ÿ˜Š' }
  }}
// => sm:hover:tw-xxxxx

This would make interpolations values the same as the possible return values of plugins and unify the api.

The function syntax (tw(({ theme, tag }) => ({ ... }) )) would still be supported to access the theme and tag functions.

Additionally this would allow to use tw as css function like in most other css-in-js libraries:

tw({
  '&::before': { content: '๐Ÿ™' }
  '&::after': { content: '๐Ÿ˜Š' }
})
// => tw-xxxxx

@bebraw @lukejacksonn What do you think? This should be settled before the initial public release (coming next week).

Variants for css objects should be supported as well:

tw`sm:${{
  '&::before': { content: '๐Ÿ™' }
  '&::after': { content: '๐Ÿ˜Š' }
}}`
// => sm:tw-xxxxx

We could introduce a css function/package for style re-use which would allow single use as well:

import { css } from 'twind/css'

const smiley = css({
  '&::before': { content: '๐Ÿ™' }
  '&::after': { content: '๐Ÿ˜Š' }
})

// smiley is an object or function (inline plugin) to lazily inject the styles on first use

smiley.valueOf()
smiley.toString()
`${smiley}`
// => tw-xxxxx

document.body.className = smiley
document.body.classList.add(smiley)

// Or use as interpolation value
tw`sm:${smiley}`
// => sm:tw-xxxxx

Styled Component API

The styled API is commonly used within the react community. Although twind is framework agnostic it may be nice to provide a styled module as well.

The API proposed here is inspired by emotion, styled-component and goober. The idea is to add two new modules:

  • twind/styled - ready to use with react
  • twind/styled/preact - ready to use with preact

The main discussion is about what type (CSS or tailwind) the styled API uses. Most editors support some CSS based styled component. Our apply method can be used within css and we could have the best of both worlds. That is why I'd like start fresh and propose the following API:

styled: thin wrapper around css

import { styled, theme } from 'twind/styled'

const Button = styled.button`
  color: ${theme('colors.gray.500)};
`

To use Tailwind within styled the @apply helper can be used:

import { styled, theme } from 'twind/styled'

const Button = styled.button`
  color: ${theme('colors.gray.500)};
  @apply bg-gray-50;
`

twind: thin wrapper around apply

To have tailwind styled component we could use a twind export (#7 (comment)):

import { twind } from 'twind/styled'

const Button = twind.button`
  text(base blue-600)
  rounded-sm
  border(& solid 2 blue-600)
  m-4 py-1 px-4
`

Support for CSS would be available via css:

import { twind, css } from 'twind/styled'

const Button = twind.button`
  text(base blue-600)
  ${css`
     some: css props;
  `}
`

styled and twind components both used an style object internally and can be used together. They differ only in how the styles are defined.


Adapting based on props

const Button = styled.button`
  color: ${(props, { theme }) => theme(props.primary ? 'colors.purple.600' : 'colors.blue.600')};
`

const Container = twind.div`flex ${(props) => props.column && 'flex-col'}`

Combine different styling #7 (comment)

// Passing a callback that receives props and returns list of various stylings
const Button = twind.button(({ primary }) => [
  // tagged template
  tw`font-bold`,

  // plain string
  'px-4 py-2 rounded-full',

  // prop condition (no need for ${} interpolation)
  primary ? 'bg-red-600 text-white' : 'bg-white text-gray-800',

  // CSS object notation
  css({ fontSize: '14pt' })
])

Styling any component

any component can be styled as long as it accepts a className prop:

const Basic = ({ className }) => <div className={className}>Some text</div>

const Fancy1 = styled(Basic)`color: darkgreen;`
const Fancy2 = twind(Basic)`text-purple-600`

Extending Styles

const Button = twind.button`
  text(base blue-600)
  rounded-sm
  border(& solid 2 blue-600)
  m-4 py-1 px-4
`

const PurpleButton = twind(Button)`
  text-purple-600 border-purple-600
`

as prop

To use styles from a styled component but change the element thatโ€™s rendered, you can use the as prop.

const Button = twind.button`text-purple-600`

render(
  <Button as="a" href="https://github.com/tw-in-js/twind">
    Twind on GitHub
  </Button>,
)

The as prop is only used by styled when itโ€™s not forwarded to the underlying element. By default, this means that the as prop is used for html tags and forwarded for components. To change this, you can pass a custom shouldForwardProp which returns true for 'as' to forward it or returns false for 'as' to use it and not forward it.

Attaching additional props

To avoid unnecessary wrappers that just pass on some props to the rendered component, or element, you can use the .attrs function. It allows you to attach additional props (or "attributes") to a component.

// we can define static props
const Input = twind.input.attrs({
  type: 'text',
})`
  rounded-md
  border(& 4 indigo-600)
`

// Or we can define dynamic ones
const SizedInput = Input.attrs((props) => ({
  size: props.size || '1em',
}))

Notice that when wrapping styled components, .attrs are applied from the innermost styled component to the outermost styled component.

This allows each wrapper to override nested uses of .attrs, similarly to how css properties defined later in a stylesheet override previous declarations.

const PasswordInput = twind(Input).attrs({
  type: 'password',
})`
  border-blue-600
`

className and class properties are merged into one string. They do not override each other.

Referring to other components

We can nest selectors using &:

const Child = twind.div`text-purple-600`
const Parent = styled.div(
  css({
    [`& ${Child}`]: {
      color: 'green',
    },
  }),
)

Currently this can only work when using CSS (either styled or css within twind)

shouldForwardProp

styled is smart enough to filter non-standard attributes automatically for you. By default, styled passes all props to custom components and only props that are valid html attributes for string tags.

You can customize this by passing a custom shouldForwardProp function. It works much like the predicate callback of Array.filter. A prop that fails the test isn't passed down to underlying components.

Optionally, shouldForwardProp can take a second parameter that provides access to the default validator function (@emotion/is-prop-valid) which is used by styled internally. This function can be used as a fallback, and of course, it also works like a predicate, filtering based on known HTML attributes.

const H1 = twind('h1', {
  shouldForwardProp: (prop, defaultValidator) => prop !== 'hidden' && defaultValidator(prop),
})`
  text-2xl underline
  ${(props) => props.hidden && 'sr-only'}
`

render(<H1 hidden>Screen Reader Only</H1>)

Refs

Passing a ref prop to a styled component will give you one of two things depending on the styled target:

  • the underlying DOM node (if targeting a basic element, e.g. twind.div)
  • a React component instance (if targeting a custom component e.g. extended from React.Component)
const Input = twind.input`
  rounded-sm m-2 p-2
  text-pink-500
  bg-yellow-200
`

const Form = () => {
  const inputRef = useRef()

  return (
    <Input
      ref={inputRef}
      placeholder="Hover to focus!"
      onMouseEnter={() => {
        inputRef.current.focus()
      }}
    />
  )
}

Using custom tw instance

styled uses the named tw export from the twind module. You can use a custom instance:

import { create } from 'twind'
import { styled } from 'twind/styled'

const { tw } = create(/* addtional config */)

const myStyled = styled.bind(tw)

/cc @tw-in-js/contributors

Ring gets override by parent shadow.

Please see this minimum markup and watch the different result between Tailwind Play and twind:

<div class="w-screen h-screen flex justify-center items-center">
  <div class="shadow-lg w-24 h-24 bg-white rounded flex justify-center items-center">
    <button class="rounded p-1 ring-1 ring-red-500">Click</button>
  </div>
</div>

Result:

image

Code:

Tailwind Play
twind

Twind's ring gets override by parent shadow.

My first thought was adding shadow-none to revert this, but watch what happen when I add shadow-none:

<div class="w-screen h-screen flex justify-center items-center">
  <div class="shadow-lg w-24 h-24 bg-white rounded flex justify-center items-center">
    <button class="shadow-none rounded p-1 ring-1 ring-red-500">Click</button>
  </div>
</div>

image

Tailwind Play is still the same, but twind's ring color disappears!

customize the generated class name prefix

Emotion uses labels for that.

let style = css`
  color: hotpink;
  label: some-name;
`

If twind would support labels, or className prefixes, we could do something similar. That way it could even work when adding labels inside the className with the shim.

pseudo:

<button class="label:PurpleButton py-4 px-8 />

And

compose`
  label:PurpleButton
  py-4 px-8 
  rounded-lg 
  bg-purple-600`
// -> .PurpleButton-XXXX 

The separator could be either a : (label:x) to be like emotion, or a - (label-x) to be more tailwind like.

Based on #73 (comment) and #73 (comment)

Extended color theme

Hey, super cool project thatโ€™s probably going to change a lot the way I currently use tailwind in some of my projects. I was already quite into Oceanwind, but this is really next level.

I quickly tried and put it head to head, reproducing the welcome example of https://play.tailwindcss.com/ into my favorite JS library and was blown away by how close it wasโ€ฆ In fact, iโ€™d even say itโ€™s 1:1.

Thereโ€™s one thing, however, that might be cool to add and I didnโ€™t see any mention in the documentation. The extended color palette introduced in tailwindย 2. As far as I understand, the default theme colors in twind is matching the default theme in tailwind, which only includes a small subset of colors. Hereโ€™s the full color list that was introduced in tailwindย 2: https://github.com/tailwindlabs/tailwindcss/blob/master/colors.js.

For file size reason, I totally get that it shouldnโ€™t be integrated within the default theme (thatโ€™s also probably why it wasnโ€™t in tailwind by default, to trim down the compilation time), but I wonder if it could be possible to add like an extra exported object that contains those rules. e.g.:

import { setup, colors } from 'twind'
// or import colors from 'twind/colors';

setup({
  theme: {
    extend: {
      colors: { cyan: colors.cyan }
    }
  }
})

The only problem I suppose is to get good tree shakeability from an object so that you only pay for the color you useโ€ฆ Iโ€™m not sure if thatโ€™d be possible. I guess you could also export each and every color separately:

import { red, cyan, blue } from 'twind/colors';
// or import * as colors from 'twind/colors';

setup({
  theme: {
    extend: {
      colors: { red, cyan, blue }
    }
  }
})

Thatโ€™s just a suggestion and I supposed one could also just copy and paste the the needed color from the original tailwind color palette as well.

Again, thanks for the library!

Overlapping classes

Hello!

Awesome work on this library, feels amazing! ๐ŸŽ‰

In React, we have a scenario which we have troubles solving using tailwindcss, but feels like we should be able to solve it using something like this. Imagine a React component that has some tailwindcss classes defined in implementation, but also receives a className prop that should be able to override it.

An example:

function Button({ className = "", children }) {
  return (
    <button className={tw("border text-white bg-blue-600", className)}>{children}</button>
  );
}

If I use this component as: <Button>blue</Button>, button should be blue, but if I try to pass bg-red-600 as className, <Button className="bg-red-600">red</Button>, then it should be red. Because of the way classes are applied (based on order in stylesheet), this isn't possible without removing bg-blue-600 from className.

I wrote simple demo available here using twind that demonstrates simple scenario that doesn't work.

Is there a theoretical way to make this work with this approach? I'd be willing to contribute with a bit of guidance. :)

Question: combining twind runtime with tailwind buildtime

I found this library in my search for runtime tailwind.
My use case is basically this that I would like to have a set of default tailwind styles added to the application beforehand and then when the user wants to change the style of an item at runtime, use twind to update the existing style tag.
I would like to prevent the import of tailwind classes with twind if I already have some classes packaged in my app.

Question is, is it possible to do such a thing with this library?

container doesn't work correctly with max-w set

here https://codesandbox.io/s/busy-moon-ypndy?file=/src/App.tsx:96-118 I set class container md:max-w-3xl which works in tailwind like that: it uses container class for screens smaller than md and uses fixed width on md and bigger.

You can test that it works in tailwind as expected if you comment this line
https://codesandbox.io/s/busy-moon-ypndy?file=/public/index.html:1033-1110
and uncomment this one
https://codesandbox.io/s/busy-moon-ypndy?file=/public/index.html:1114-1238

Adopted stylesheets support

It would be useful if we could target a constructed stylesheet which we can then use via shadowRoot.adoptedStyleSheets = [twindSheet].

This would allow us to create the stylesheet once and re-use it across multiple web components without having the side effect of polluting the root document's styles.

I've had a read through the source and it seems the current "sheet" implementation is using CSSOM to add rules to a stylesheet already. However, its default target is a style tag appended to the DOM.

So maybe its as simple as having a different target in the existing cssom sheet? A reference to a sheet rather than one we have in DOM.

Though then the question is, how do we get hold of that per web component?

class SomeElement extends HTMLElement {
  // ...

  connectedCallback() {
    // how do we get hold of this sheet?
    this.shadowRoot.adoptedStyleSheets = [REF_TO_TWIND_SHEET];
  }

  // Assume this just ends up appending to this.shadowRoot
  render() {
    return `<div class="${tw`bg-black`}">Foo</div>`;
  }
}

As you can see, we'd probably need to create the stylesheet ourselves and tell twind to append to it. Like during setup() of twind.

Though that leaves us with a choice to make too:

  • a monolithic global twind sheet (i.e. we can only setup() once, which means we can only have one tailwind stylesheet to contain everything)
  • somehow per-module tell twind which sheet to use (like a setup() per es module)

Could do with some christmas coding so would be happy to have a go at this once i get my head around it

Component Proposal

Thank you all for the feedback, questions, ideas and suggestions. I have edited the proposed solution o reflect the discussion.
Proposed Draft 2020-13-01 04:50 GMT


Related issues #7, #60

While working on twind/styled (PR #7) i realized that twind does not have a good component composition model. With component composition we mean re-using styles for several components while allowing to override certain styles like changing the background color.

Problem Statement

As a component author I want to re-use tailwind directive styles for defining my component and allow users of the component to override styles using tailwind directive. Additionally I want to be able to extend a component and override or add some styles using tailwind rules.

The problem we try to solve is component based composition while tw should keep the expected tailwind behavior.

One way to do composition is utility combinations to recreate the same component in many different places (see Extracting Components). I would call this class composition as it applies or groups several class names for a component.

Details with an example and its problems
const Button = ({ className, children}) => {
  return <button className={tw`inline-block bg-gray-500 text-base ${className}`}>{children}</button>
}

const ButtonBlock = ({ className, children}) => {
  return <Button className={`block ${className}`}>{children}</Button>
}

<Button>gray-500</Button>
<Button className="bg-red-500 text-lg">red-500 large</Button>

The example above does not reliably work because the injected css classes have all the same specificity and therefore the order they appear in the stylesheet determine which styles are applied.

It is really difficult to know which directive does override another. Lets stick with bg-* but there are others. The bg prefix and its plugin handle several css properties where background-color is only one of them.

This ambiguity makes class based composition really difficult. That was the reason we introduced the override variant.

Consider the following twind/styled (PR #7) example:

const Button = twind.button`
  text(base blue-600)
  rounded-sm
  border(& solid 2 blue-600)
  m-4 py-1 px-4
`

// Create a child component overriding some colors
const PurpleButton = twind(Button)`
  override:(text-purple-600 border-purple-600)
`

As you see it is difficult to override certain utility classes on usage or when creating a child component. For this to work twind introduced the override variant which increases the specificity of the classes it is applied to. But what do you do for a grandchild component or if you want to override the PurpleButton styles? override:override:...? There must be a better way to solve this problem.

tailwind has a component concept using @apply which basically merges the css rules of several tailwind classes into one class. twin.macro does the same.

That is something I would call style composition and is currently not available in twind.

Details of tailwind @apply

Tailwindcss provides @apply to extract component classes which merges the underlying styles of the utility classes into a single css class. That is something i would call style composition and is currently not available in twind.

.btn-indigo {
  @apply py-2 px-4 bg-indigo-500 text-white font-semibold rounded-lg shadow-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-400 focus:ring-opacity-75;
}

twind.macro does the same during build time to generate css-in-js objects which are evaluated with a runtime like emotion or styled-component:

const hoverStyles = css`
  &:hover {
    border-color: black;
    ${tw`text-black`}
  }
`
const Input = ({ hasHover }) => (
  <input css={[tw`border`, hasHover && hoverStyles]} />
)

The tw function from twin.macro acts like the @apply helper from tailwindcss.

Lets summarize both composition approaches:

  • class: apply several utility classes on an element
  • style: extract utility css declarations and merge them into one css class

Proposed Solution

Recap of available APIs in twind and their transformations:

  • tw: one tailwind rule => one class name โ€“ with side effect of inserting the css into the stylesheet
  • css: css rules => one class name (via tw) โ€“ lazy evaluated (injected by tw on first use)

When i look at this i see a missing piece:

  • tw.apply: several tailwind rules => one class name (via tw) โ€“ lazy evaluated (injected by tw on first use)

    All considers names
    • tw.apply => to mirror tailwindcss @apply
    • css.of => as it create one big css object basically
    • translate => as it translate tailwind rules to an css object
    • compose => as it merges tailwind rules together
const btn = tw.apply`inline-block bg-gray-500 text-base`
// => generates on css class with all declarations of the above rules when used

const btnBlick = tw.apply`${btn} block`
// => generates on css class with all declarations of btn & block
// Never used => never injected

<button class={tw`${btn}`}>gray-500</button>
// => tw-btn

<button class={tw`${btn} bg-red-500 text-lg`}>red-500 large</button>
// => tw-btn bg-red-500 text-lg

That API needs to

  • generate one style object eg one css class combining all tailwind rules by deep merging rules in order of declaration

  • allow utility classes applied on the same element override its styles; eg styles are injected after base (preflight) and before utility classes

  • can be used with tw => tw(tw.apply(...)); eg implement as an inline directive

  • allow to inject the styles and access the class name without calling tw => result.toString() and result.valueOf()

  • support template literal, strings, arrays, objects and other inline directives (incl css) as parameters

    Rule Precedence Calculation

    To have a predictable styling the styles must be ordered.

    This order is represented by a precedence number. The lower values are inserted before higher values. Meaning higher precedence styles overwrite lower precedence styles.

    Each rule has some traits that are put into a bit set which form the precedence:

    bits trait
    1 dark mode
    2 layer: base = 0, components = 1, utilities = 2 , css = 3
    1 screens: is this a responsive variation of a rule
    5 responsive based on min-width
    4 at-rules
    17 pseudo and group variants
    4 number of declarations (descending)
    4 greatest precedence of properties

    Dark Mode: 1 bit

    Flag for dark mode rules.

    Layer: 3 bits

    • base = 0: The preflight styles and any base styles registered by plugins.
    • components = 1: Component classes and any component classes registered by plugins.
    • utilities = 2: Utility classes and any utility classes registered by plugins.
    • css = 3: Inline plugins

    Screens: 1 bit

    Flag for screen variants. They may not always have a min-width to be detected by Responsive below.

    Responsive: 5 bits

    Based on extracted min-width value:

    • 576px -> 3
    • 1536px -> 9
    • 36rem -> 3
    • 96rem -> 9

    At-Rules: 4 bits

    Based on the count of special chars (-:,) within the at-rule.

    Pseudo and group variants: 17 bits

    Ensures predictable order of pseudo classes.

    Number of declarations (descending): 4 bits

    Allows single declaration styles to overwrite styles from multi declaration styles.

    Greatest precedence of properties: 4 bits

    Ensure shorthand properties are inserted before longhand properties; eg longhand override shorthand

  • be lazy evaluated because it may never be used

    Why lazy?

    For one to prevent unnecessary style injection and to prevent problems when importing a component library that uses this API before invoking setup.

Here are some examples using tw.apply to get a feeling for the API:

Basic usage

Please note that the utility classes are always defined after the component styles which allows them to overrides certain styles.

import { tw } from 'twind'

const btn = tw.apply`
  py-2 px-4
  font-semibold
  rounded-lg shadow-md
  focus:(outline-none ring(2 indigo-400 opacity-75))
`

tw`${btn} font-bold`
// => .tw-btn .font-bold
// CSS:
// .tw-XXXX { padding-top: 0.5rem; padding-bottom: 0.5rem; padding-left: 1rem; padding-right: 1rem; font-weight: 600; ...}
// .font-bold { font-weight: 700; }

const btnLarge = tw.apply`${btn} py-4 px-8`
// Result: () => ({ paddingTop: '1rem', paddingBottom: '1rem', paddingLeft: '2rem', paddingRight: '2rem', fontWeight: '600', ... })

tw`${btnLarge} rounded-md`
// => .tw-btn-large .rounded-md
// CSS:
// .tw-btn-large { padding-top: 1rem; padding-bottom: 1rem; padding-left: 2rem; padding-right: 2rem; font-weight: 600; ... }
// .rounded-md { ... }
twin.macro and styled-component compatibility eg generate one class

The would be possible as the returned function has toString and valueOf methods which inject the styles and return the class name:

<button className={tw.apply`bg-red bg-blue`}>blue</button>
// => tw-red-blue

document.body.className = tw.apply`bg-blue bg-red`
// => tw-blue-red

Or use this helper:

// There is a better name out there somewhere
const twind = (...args) => tw(tw.apply(...args))

<button className={twind`bg-red bg-blue`}>blue</button>
// => tw-red-blue

document.body.className = twind`bg-blue bg-red`
// => tw-blue-red
`css` can be used within `tw.apply`
const btn = tw.apply`
  py-2 px-4
  ${css({
    borderColor: 'black',
  })}
`
Using within css โ€“ pending

tw.apply can be used with css ( (pending variable arguments, array support):

const prose = css(
  tw.apply`text-gray-700 dark:text-gray-300`,
  {
    p: tw.apply`my-5`,
    h1: tw.apply`text-black dark:text-white`,
  },
  {
    h1: {
      fontWeight: '800',
      fontSize: '2.25em',
      marginTop: '0',
      marginBottom: '0.8888889em',
      lineHeight: '1.1111111',
    }
  }
)

Using template literal syntax (pending, but i'm working on it):

const prose = css`
  ${tw.apply`text-gray-700 dark:text-gray-300`)

  p { ${tw.apply('my-5')} }

  h1 {
    ${tw.apply`text-black dark:text-white`}
    font-weight: 800;
    font-size: 2.25em;
    margin-top: 0;
    margin-bottom: 0.8888889em;
    line-height: 1.1111111;
  }
`
`twind/styled` would then be a small react wrapper around `tw.apply`
const Button = twind.button`
  text(base blue-600)
  rounded-sm
  border(& solid 2 blue-600)
  m-4 py-1 px-4
`

const PurpleButton = twind(Button)`
  text-purple-600 border-purple-600
`
Using tailwind directives with `animation` from `twind/css`
const motion = animation('.6s ease-in-out infinite', {
  '0%': tw.apply`scale-100`,
  '50%': tw.apply`scale-125 rotate-45`,
  '100%': tw.apply`scale-100 rotate-0`,
})
A react button component
import { tw } from 'twind'

const variantMap = {
  success: "green",
  primary: "blue",
  warning: "yellow",
  info: "gray",
  danger: "red"
}

const sizeMap = {
  sm: tw.apply`text-xs py(2 md:1) px-2`,
  md: tw.apply`text-sm py(3 md:2) px-2`,
  lg: tw.apply`text-lg py-2 px-4`,
  xl: tw.apply`text-xl py-3 px-6`
}

const baseStyles = tw.apply`
  w(full md:auto)
  text(sm white uppercase)
  px-4
  border-none
  transition-colors
  duration-300
`

function Button({ size = 'md', variant = "primary", round = false, disabled = false, className, children }) {
  // Collect all styles into one class
  const instanceStyles = tw.apply`
    ${baseStyles}
    bg-${variantMap[variant]}(600 700(hover:& focus:&)))
    ${sizeMap[size]}
    rounded-${round ? "full" : "lg"}
    ${disabled && "bg-gray-400 text-gray-100 cursor-not-allowed"}
  `
  
  // Allow passed classNames to override instance styles
  return <button className={tw(instanceStyles, className)}>{children}</button>
}

render(<Button variant="info" className="text-lg rounded-md">Click me</Button>)

Discared Proposed Solutions

1. Nested tw (#73 (comment))

  • tw by itself behaves as it does now, untouched
  • nested tw has a new behavior
tw`bg-red bg-blue`;
// css .bg-red {}, .bg-blue {} are appended
// result is bg-red bg-blue

const base = tw`bg-red`;
// css .bg-red {} is NOT appended as it already was on line 1
// result is bg-red

tw`${base} bg-blue`;
// css .tw-generated-bg-blue{} is appended
// result is bg-red tw-generated-bg-blue

Open question @43081j: How to ensure that generated-bg-blue has a higher precedence than bg-red?

2. Reverse Translation (#73 (comment))

Enhance tw to detect directives that override previous ones and omit those from the result class names string.

const btn = tw`py-2 px-4 font-semibold`
// => py-2 px-4 font-semibold

tw`${btn} py-4 px-8`
// => font-semibold py-4 px-8

tw`py-4 ${btn} px-8`
// => py-2 font-semibold px-8

Algorithm

  1. transform all rules to their css equivalent
  2. merge all css into one object
  3. for each rule check if their css is contained within the css object; if that is the case include it in the output

Step 1 and 2 are possible. Step 3 may have some edge cases like what to do if the css is a partial match:

.bg-red-500 {
  --tw-bg-opacity: 1;
  background-color: rgba(239, 68, 68, var(--tw-bg-opacity));
}

.bg-opacity-5 {
  --tw-bg-opacity: 0.05;
}

bg-opacity-5 partially overrides bg-red-500. Both must be included in the output.

Another edge case may be if the css helper is used. And i'm sure there a some i haven't identified yet.

3. twind/compose

Introduce compose as a new function which would extract the styles of the provided directives and returns an inline directive with an css style object containing all deep merged rules which can be used with tw. The generated styles would have a lower precedence than the utility classes which would allow to use tailwind directives to override styles.

The following examples use template literals but well known tw arguments like strings, arrays, objects, and inline directives, would be supported.

import { compose } from 'twind/compose'

const btn = compose`
  py-2 px-4
  font-semibold
  rounded-lg shadow-md
  focus:(outline-none ring(2 indigo-400 opacity-75))
`
// Result: () => ({ paddingTop: '0.5rem', paddingBottom: '0.5rem', paddingLeft: '1rem', paddingRight: '1rem', fontWeight: '600', ... })
// CSS:
// .tw-XXXX { padding-top: 0.5rem; padding-bottom: 0.5rem; padding-left: 1rem; padding-right: 1rem; font-weight: 600; ...}

const btnLarge = compose`${btn} py-4 px-8`
// Result: () => ({ paddingTop: '1rem', paddingBottom: '1rem', paddingLeft: '2rem', paddingRight: '2rem', fontWeight: '600', ... })
// CSS:
// .tw-YYYY { padding-top: 1rem; padding-bottom: 1rem; padding-left: 2rem; padding-right: 2rem; font-weight: 600; ... }

tw`${btnLarge} rounded-md`
// => .tw-btn .tw-btn-large .rounded-md

css can be used within compose:

const btn = compose`
  py-2 px-4
  ${css({
    borderColor: 'black',
  })}
`
Using within css โ€“ pending (Click to expand)

compose can be used with css ( (pending variable arguments, array support):

const prose = css(
  compose`text-gray-700 dark:text-gray-300`,
  {
    p: compose`my-5`,
    h1: compose`text-black dark:text-white`,
  },
  {
    h1: {
      fontWeight: '800',
      fontSize: '2.25em',
      marginTop: '0',
      marginBottom: '0.8888889em',
      lineHeight: '1.1111111',
    }
  }
)

Using template literal syntax (pending, but i'm working on it):

const prose = css`
  ${compose`text-gray-700 dark:text-gray-300`)

  p { ${compose('my-5')} }

  h1 {
    ${compose`text-black dark:text-white`}
    font-weight: 800;
    font-size: 2.25em;
    margin-top: 0;
    margin-bottom: 0.8888889em;
    line-height: 1.1111111;
  }
`

twind/styled would then be a small react wrapper around the base compose:

const Button = twind.button`
  text(base blue-600)
  rounded-sm
  border(& solid 2 blue-600)
  m-4 py-1 px-4
`

const PurpleButton = twind(Button)`
  text-purple-600 border-purple-600
`
Show more examples (click to expand)

Using tailwind directives with animation from twind/css:

const motion = animation('.6s ease-in-out infinite', {
  '0%': compose`scale-100`,
  '50%': compose`scale-125 rotate-45`,
  '100%': compose`scale-100 rotate-0`,
})

Here is an example for an react button component:

import { tw } from 'twind'
import { compose } from 'twind/compose'

const variantMap = {
  success: "green",
  primary: "blue",
  warning: "yellow",
  info: "gray",
  danger: "red"
}

const sizeMap = {
  sm: compose`text-xs py(2 md:1) px-2`,
  md: compose`text-sm py(3 md:2) px-2`,
  lg: compose`text-lg py-2 px-4`,
  xl: compose`text-xl py-3 px-6`
}

const baseStyles = compose`
  w(full md:auto)
  text(sm white uppercase)
  px-4
  border-none
  transition-colors
  duration-300
`

function Button({ size = 'md', variant = "primary", round = false, disabled = false, className, children }) {
  const instanceStyles = compose`
    ${baseStyles}
    bg-${variantMap[variant]}(600 700(hover:& focus:&)))
    ${sizeMap[size]}
    rounded-${round ? "full" : "lg"}
    ${disabled && "bg-gray-400 text-gray-100 cursor-not-allowed"}
  `
  
  // Allow passed classNames to override instance styles
  return <button className={tw(instanceStyles, className)}>{children}</button>
}

render(<Button variant="info" className="text-lg rounded-md">Click me</Button>)

4. Enhance twind/css

Extend twind/css to extract the styles of the provided directives and return an inline directive with an css style object containing all deep merged rules which can be used with tw. The generated styles would have a lower precedence than the utility classes which would allow to use tailwind directives to override styles.

css currently accepts an css object. We could extend it to accept strings which are directives:

css would now be a translator from tailwind rules to css object.

Please note that the template literal syntax may come with issues in editors and prettier as it may be mistaken for real css. If anyone has a solution please comment below.

const btn = css('py-2 px-4 font-semibold')
// => { padding-top: 0.5rem; padding-bottom: 0.5rem; padding-left: 1rem; padding-right: 1rem; font-weight: 600; }

tw`${btn} py-4 px-8`
// => tw-xxx py-4 px-8

const largeBtn = css`${btn} py-4 px-8`
// => { padding-top: 1rem; padding-bottom: 1rem; padding-left: 2rem; padding-right: 2rem; font-weight: 600; }
tw`${largeBtn} font-bold`
// => tw-yyyy font-bold
Show more examples (click to expand)

Using tailwind directives with animation from twind/css:

import { css, animation } from 'twind/css'

const motion = animation('.6s ease-in-out infinite', {
  '0%': css('scale-100'),
  '50%': css('scale-125 rotate-45'),
  '100%': css('scale-100 rotate-0'),
})

Here is an example for an react button component:

import { tw } from 'twind'
import { css } from 'twind/css'

const variantMap = {
  success: "green",
  primary: "blue",
  warning: "yellow",
  info: "gray",
  danger: "red"
}

const sizeMap = {
  sm: css('text-xs py(2 md:1) px-2'),
  md: css('text-sm py(3 md:2) px-2'),
  lg: css('text-lg py-2 px-4'),
  xl: css('text-xl py-3 px-6')
}

const baseStyles = css(`
  w(full md:auto)
  text(sm white uppercase)
  px-4
  border-none
  transition-colors
  duration-300
`)

function Button({ size = 'md', variant = "primary", round = false, disabled = false, className, children }) {
  const instanceStyles = css(`
    ${baseStyles}
    bg-${variantMap[variant]}(600 700(hover:& focus:&)))
    ${sizeMap[size]}
    rounded-${round ? "full" : "lg"}
    ${disabled && "bg-gray-400 text-gray-100 cursor-not-allowed"}
  `)
  
  // Allow passed classNames to override instance styles
  return <button className={tw(instanceStyles, className)}>{children}</button>
}

render(<Button variant="info" className="text-lg rounded-md">Click me</Button>)

Summary

I hope have summarized all sides of the discussion and everybody sees theirs points reflected in the proposed solution.


Thank you for reading this whole thing โค๏ธ

/cc @tw-in-js/contributors

API considerations

Taxomony

  • token: things that can be passed to tw (string, array, object, falsey values)
  • rules: rule, group
  • rule: variants, directive
  • directive: negate, plugin, params
  • negate: should a theme value be negated
  • plugin: utility, component
  • params: parameters for plugins; usually the key in the theme

Architecture

  1. setup - create theme and stuff

Phases (details below)

  1. parse rules
  2. translate each rule to css object using plugins
  3. decorate add variant rules to the css object (basicly wrapping it again and again)
  4. serialize convert css object into css declarations with precedence and hash
  5. inject css rules

Feature evaluation based on:

  • size limit (current stats: 10.5kb)
  • performance budget

API

common types used below

type Falsy = '' | 0 | -0 | false | null | undefined

interface TokenGrouping extends Record<string, Token> {}

type Token = string | TokenGrouping | Token[] | Falsy

interface Context {
  /** allow composition */
  tw: TW
  
  /** access to theme values */
  theme(section: string): ThemeSection
  theme(section: string, key: string | string[]): string | undefined
  theme(section: string, key: string | string[], defaultValue: string): string

  /** create unique identifier (group, custom properties) */
  tag(key: string): string
}

tw

converts rules into CSS class names

  • exposed named export
  • supports
    • tagged template where replacements can be interpolations can be strings, array, objects, falsey
    • variadic strings, array, objects, falsey
interface TW {
  (strings: string[], ...replacements: Token[]): string
  (...rules: Token[]): string
}

setup

customize tw

  • default to dev settings
    • with example for recommended prod settings
  • setup can be called only once
    • warn setup called after tw
interface Setup {
  preflight?: boolean | Preflight
  
  mode?: Mode
  
  theme?: Partial<Theme> | ((defaultTheme: ThemeResolver) => Partial<Theme>)

  hash?: boolean | Hasher
  
  plugins?: Record<string, string | CSSRules | Plugin>
  
  injector?: Injector
  
  prefix?: boolean | Prefix
}

preflight

Add global styles before first rule is injected
default: tailwind preflight

  • false
  • function - allow to use a different reset
interface Preflight {
  (preflight: CSSRules, context: Context): CSSRules | undefined
}

mode

customize behaviour
default: warn

  • warn
  • strict
  • play
interface Mode {
  /**
   * Notify error (missing plugin, duplicate directives? )
   *
   * Why id?
   * - can generate an url with more info
   * - reduce bundle size by omitting large error messages
   */
  report(info: {id: string}, context: Context): void
  
  /** Called for unknown theme values */
  unknown(
    section: string,
    key: string,
    optional: boolean,
    context: Context,
  ): string | undefined
}

hash

default: false

  • boolean

theme

default: tailwind

  • mergeing and extending like tailwind compatible

plugins

allows to define/override plugins
default: core plugins

Plugins are searched for by name using the longest prefix before a dash ("-"'). The name and the remaining parts (splitted by a dash) are provided as first argument to the plugin function. For example if the directive is bg-gradient-to-t the following order applies:

Plugin Params
bg-gradient-to-t []
bg-gradient-to ["t"]
bg-gradient ["to", "t"]
bg ["gradient", "to", "t"]

Arguments

  1. params
  2. context
  3. id

Return Types

  • CSSRules: css rules to inject (must use & for nested selectors)
  • string: class name to use
    • group and lead return a marker class name that should be used as is
  • falsy: no result -> mode.unknown() then mode.report()
interface Plugin {
  /**
   * Creates CSSRules based on `parts`
   */
  (params: string[], context: Context, id: string): CSSRules | string | Falsy
}

Examples

const plugins = {
  block: { display: 'block' },

  placeholder: (params, { theme }) => {
    const value = theme('placeholderColor', params)
  
    return value && {
      '&::placholder': {
        color: value
      },
    }
  },

  rotate: (params, { theme }) => {
    const value = theme('rotate', tail(params))
  
    return value && {
      '--tw-rotate': value,
      transform: [
        `rotate(${value})`,
        `translateX(var(--tw-translate-x),0)) translateY(var(--tw-translate-y),0) rotate(var(--tw-rotate,0))`
      ],
    }
  },
  
  /** use tag to create marker class */
  group: (params, { tag }) => tag('group'),
  
  /** use tw to compose */
  card: (params, { tw }) => tw`max-w-md mx-auto bg-white`
}

injector

insert CSS rules into runtime
default: based on enviroment

  • dom
  • collect - for testing in @tw-in-js/test
  • ssr - move to @tw-in-js/server
interface Injector {
  insert(rule: string, index: number): void
  delete(index: number): void
}

prefix

CSS prefixing
default: based on tiny-css-prefixer

  • false
  • function
interface Prefixer {
  (property: string, value: string): string
}

Browser Support

  • IE11: section how to make it work
  • document used modern APIs which need to be polyfilled
    • Math.imul
  • reset as extra package
  • fallbacks included with deprection notice

Phases

Parse

parse tokens in rules

interface Parse {
  (strings: string[], replacements: Token[]): Rule[]
  (...rules: Token[]): Rule[]
}

type Token = string | Falsy | Record<string, Token> | Token[]

interface Rule {
  /** ["sm", "dark", "hover"] */
  variants: string[]
  
  /** "text-sm", "-rotate-45" */
  directive: string
}
character = letter | digit;
identifier = character, { character };
whitespace = ' ' | '\t' | [ '\r' ], '\n';

(*
- "h-full bg-purple-500 rotate-3 scale-95"
- "w(1/2 sm:1/3 lg:1/6) p-2"
- "divide(y-2 blue-500 opacity(75 md:50))"
- "ring(& ping-700 offset(4 ping-200))"
- "rotate(-3 hover:6 md:(3 hover:-6))"
*)
rules =
    { whitespace },
    group | rule,
    { { whitespace }-, group | rule },
    { whitespace } ;

group = variantGroup | directiveGroup;

variantGroup =
    { variant, ":" }-,
    '(',
        { rules }-,
    ')';

directiveGroup =
    { variant, ":" },
    plugin,
    '(',
        { "&" | rules }-,
    ')';

rule = { variant, ":" }, directive;

variant = identifier;

directive = [ "-" ], plugin, { "-", params };

plugin = identifier, { "-", identifier };

params = param, { "-", param };

param = identifier | "_" | "/" | ".";

Translate

translate rules into CSSRules using plugins

  • no on-colors - maybe as plugin

Why CSSRules?

  • can be typed with csstype
  • allow to implement advanced directives like container, placeholder, divide, prose, ...
  • allows to add global default custom properties - this may lead to allow automatic integration tests comparing our result with the ones from tailwind
interface Translate {
  (rule: Rule, context: Context): CSSRules
}

import * as CSS from 'csstype'

interface CSSProperties extends CSS.PropertiesFallback, CSS.PropertiesHyphenFallback {}

interface CSSRules extends CSSProperties {
  /** @media, @supports, @keyframes, ... */
  [`@${string}`]: CSSRules
  
  /** pseudo class
   * maybe -> could be implement using '&:'
   * watch out for ':root' -> that could use '*' instead
   */
  [`:${string}`]: CSSRules

  /** maybe allow '&' everywhere */
  [`${string}&${string}`]: CSSRules

  /** global defaults */
  '*': CSSProperties
}

Inject

insert CSSRules into the enviroment returning a string with class names

  • not atomic
  • position based on precedence
  • hash class names and CSS custom properties
interface Inject {
  /** we need the parsed rule for class name generation and precedence calculation */
  (css: CSSRules, rule: Rule | undefined, context: Context): string
}

Packages

monorepo

name: why not go with ocean/beamwind

  • @tw-in-js/core - not really happy with the name; but i have no alternative
  • @tw-in-js/prose
  • @tw-in-js/server - for ssr
  • @tw-in-js/typescript-plugin

Development

  • typescript
  • es2020 syntax, transpile with esbuild
  • snowpack preview

Tailwind Differences

  • group-* variant works for every pseudo class
    This allows to use group-focus-with or group-active

  • automatically infers negated values - they do not need to be in the theme config

  • bg-gradient-to-* is built-in, no need to configure these

  • text-underline, text-no-underline, text-line-through, text-uppercase, text-lowercase and text-capitalize: this allows grouping of text directives like text(lg red-500 capitalize underline)

  • font-italic and font-no-italic: this allows grouping of font directives like font(sans italic bold)

  • border and divide allow to combine positions (top, rrigh, left, bottom)

    • tr - top & right
    • brl - bottom, right and left

    Note x and y can not be combined.

  • rotate, scale , skew and translate provide a fallback for IE 11

    transform rotate-45 works but when using transform rotate-45 scale-150 only one of both is applied.

IE 11 compatibility

Some new tailwind features use CSS Variables (Custom Properties) and are therefore not compatible with IE 11.

tw-in-js includes fallbacks for the following directives which mimic Tailwind v1 behavior:

Some directive only work with CSS Variables and are not supported in IE 11:

Ideas

  • react to manual class name change in dev-tools and generate that directive
  • live theme updates
  • track used theme values and re-translate
  • @tw-in-js/styled - like styled components
  • @tw-in-js/eslint-plugin - nice to have
  • @tw-in-js/tw.macro - maybe?
  • svelte, preact, react, vue demos

Deno support

So far it seems like twind works with client side Deno. Either shim or regular (I also tested https://cdn.esm.sh/twind, https://jspm.dev/twind). This is not that surprising given that twind already works as es module in the browser :)

import 'https://cdn.skypack.dev/twind/shim'
import { tw, setup } from 'https://cdn.skypack.dev/twind'

As for server-side support, the first issue is that it just crashes :)

https://cdn.esm.sh/twind/server
https://jspm.dev/twind/server
https://cdn.skypack.dev/twind/server

I guess it happens because of dependency on async rendering for node.

So I tried to create a simple server on Deno that could run on codesanbox https://codesandbox.io/s/deno-twinddev-ssr-p4l2h?file=/src/index.ts

This one is actually not that bad :) it crashes if I try to update to the latest version, but otherwise does most of what I expected twind/server to do :)

p.s. I personally was trying it out in aleph.js but it comes with its own complications that probably would require a separate issue :)

Support pseudo elements

Pseudo elements like ::before and ::after are denoted with a double colon. These can be currently only supported defining them as additional variants (setup({variants}) and core variants):

setup({
  variants: {
    before: '&::before',
    after: '&::after',
  }
})

tw`before:block`

To support these within Twind I see two options:

  1. Adding them to the core variants (see PR #106):
{
  after: '&::after',
  before: '&::before',
  'first-letter': '&::first-letter',
  'first-line': '&::first-line',
  selection: '&::selection',
}

Usage:

tw`before:block`
  1. Create plugin providing the variants
import { pseudoElements } from '@twind/pseudo-elements'

setup({
  variants: pseudoElements
})

tw`before:block`
  1. Extend the parser using double colon after the variant (see PR #105):
tw`before::block`

Which options do you prefer? Or do you see another way?

/cc @tw-in-js/contributors

TypeScript Issues with Styled API for React

I've noticed a few TypeScript issues with the styled API for React:

  • Props that should be allowed are not (ex. type prop on Input )
  • Props argument for inline functions is not strongly typed (any)
  • Props that are not defined in the type definitions are not being caught

Iโ€™ve included a minimal example that demonstrates each of these issues. It also includes a styled-components implementation for comparison.

Codesandbox Link: https://codesandbox.io/s/twind-styled-typescript-by9l4

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.