Tailwind v4 is not seeing classes in slang templates
What version of Tailwind CSS are you using?
For example: v4.1.4
What build tool (or framework if it abstracts the build tool) are you using?
None, using tailwindcss cli from Archlinux AUR package.
What version of Node.js are you using?
None
What browser are you using?
Chromium, Firefox
What operating system are you using?
Linux
Reproduction URL
Basic tailwindcss input.css file:
@import "tailwindcss";
Basic test.slang file:
doctype html
html
head
link rel="stylesheet" href="style.css"
body.min-h-screen
header.stick.top-0.z-10
Run tailwindcss -i input.css -o style.css and check that there will be no .z-10 CSS class there.
The resulting style.css is:
/*! tailwindcss v4.1.4 | MIT License | https://tailwindcss.com */
@layer theme, base, components, utilities;
@layer theme {
:root, :host {
--font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace;
--default-font-family: var(--font-sans);
--default-mono-font-family: var(--font-mono);
}
}
@layer base {
*, ::after, ::before, ::backdrop, ::file-selector-button {
box-sizing: border-box;
margin: 0;
padding: 0;
border: 0 solid;
}
html, :host {
line-height: 1.5;
-webkit-text-size-adjust: 100%;
tab-size: 4;
font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji');
font-feature-settings: var(--default-font-feature-settings, normal);
font-variation-settings: var(--default-font-variation-settings, normal);
-webkit-tap-highlight-color: transparent;
}
hr {
height: 0;
color: inherit;
border-top-width: 1px;
}
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
h1, h2, h3, h4, h5, h6 {
font-size: inherit;
font-weight: inherit;
}
a {
color: inherit;
-webkit-text-decoration: inherit;
text-decoration: inherit;
}
b, strong {
font-weight: bolder;
}
code, kbd, samp, pre {
font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace);
font-feature-settings: var(--default-mono-font-feature-settings, normal);
font-variation-settings: var(--default-mono-font-variation-settings, normal);
font-size: 1em;
}
small {
font-size: 80%;
}
sub, sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
table {
text-indent: 0;
border-color: inherit;
border-collapse: collapse;
}
:-moz-focusring {
outline: auto;
}
progress {
vertical-align: baseline;
}
summary {
display: list-item;
}
ol, ul, menu {
list-style: none;
}
img, svg, video, canvas, audio, iframe, embed, object {
display: block;
vertical-align: middle;
}
img, video {
max-width: 100%;
height: auto;
}
button, input, select, optgroup, textarea, ::file-selector-button {
font: inherit;
font-feature-settings: inherit;
font-variation-settings: inherit;
letter-spacing: inherit;
color: inherit;
border-radius: 0;
background-color: transparent;
opacity: 1;
}
:where(select:is([multiple], [size])) optgroup {
font-weight: bolder;
}
:where(select:is([multiple], [size])) optgroup option {
padding-inline-start: 20px;
}
::file-selector-button {
margin-inline-end: 4px;
}
::placeholder {
opacity: 1;
}
@supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {
::placeholder {
color: currentcolor;
@supports (color: color-mix(in lab, red, red)) {
color: color-mix(in oklab, currentcolor 50%, transparent);
}
}
}
textarea {
resize: vertical;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-date-and-time-value {
min-height: 1lh;
text-align: inherit;
}
::-webkit-datetime-edit {
display: inline-flex;
}
::-webkit-datetime-edit-fields-wrapper {
padding: 0;
}
::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
padding-block: 0;
}
:-moz-ui-invalid {
box-shadow: none;
}
button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-button {
appearance: button;
}
::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
height: auto;
}
[hidden]:where(:not([hidden='until-found'])) {
display: none !important;
}
}
@layer utilities {
.z-10 {
z-index: 10;
}
.min-h-screen {
min-height: 100vh;
}
}
If I change the slang template to
doctype html
html
head
link rel="stylesheet" href="style.css"
body.min-h-screen
header class="stick top-0 z-10"
The result is ok, but I'm using slang to not have to declare classes this way.
/*! tailwindcss v4.1.4 | MIT License | https://tailwindcss.com */
@layer theme, base, components, utilities;
@layer theme {
:root, :host {
--font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace;
--spacing: 0.25rem;
--default-font-family: var(--font-sans);
--default-mono-font-family: var(--font-mono);
}
}
@layer base {
*, ::after, ::before, ::backdrop, ::file-selector-button {
box-sizing: border-box;
margin: 0;
padding: 0;
border: 0 solid;
}
html, :host {
line-height: 1.5;
-webkit-text-size-adjust: 100%;
tab-size: 4;
font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji');
font-feature-settings: var(--default-font-feature-settings, normal);
font-variation-settings: var(--default-font-variation-settings, normal);
-webkit-tap-highlight-color: transparent;
}
hr {
height: 0;
color: inherit;
border-top-width: 1px;
}
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
h1, h2, h3, h4, h5, h6 {
font-size: inherit;
font-weight: inherit;
}
a {
color: inherit;
-webkit-text-decoration: inherit;
text-decoration: inherit;
}
b, strong {
font-weight: bolder;
}
code, kbd, samp, pre {
font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace);
font-feature-settings: var(--default-mono-font-feature-settings, normal);
font-variation-settings: var(--default-mono-font-variation-settings, normal);
font-size: 1em;
}
small {
font-size: 80%;
}
sub, sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
table {
text-indent: 0;
border-color: inherit;
border-collapse: collapse;
}
:-moz-focusring {
outline: auto;
}
progress {
vertical-align: baseline;
}
summary {
display: list-item;
}
ol, ul, menu {
list-style: none;
}
img, svg, video, canvas, audio, iframe, embed, object {
display: block;
vertical-align: middle;
}
img, video {
max-width: 100%;
height: auto;
}
button, input, select, optgroup, textarea, ::file-selector-button {
font: inherit;
font-feature-settings: inherit;
font-variation-settings: inherit;
letter-spacing: inherit;
color: inherit;
border-radius: 0;
background-color: transparent;
opacity: 1;
}
:where(select:is([multiple], [size])) optgroup {
font-weight: bolder;
}
:where(select:is([multiple], [size])) optgroup option {
padding-inline-start: 20px;
}
::file-selector-button {
margin-inline-end: 4px;
}
::placeholder {
opacity: 1;
}
@supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {
::placeholder {
color: currentcolor;
@supports (color: color-mix(in lab, red, red)) {
color: color-mix(in oklab, currentcolor 50%, transparent);
}
}
}
textarea {
resize: vertical;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-date-and-time-value {
min-height: 1lh;
text-align: inherit;
}
::-webkit-datetime-edit {
display: inline-flex;
}
::-webkit-datetime-edit-fields-wrapper {
padding: 0;
}
::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
padding-block: 0;
}
:-moz-ui-invalid {
box-shadow: none;
}
button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-button {
appearance: button;
}
::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
height: auto;
}
[hidden]:where(:not([hidden='until-found'])) {
display: none !important;
}
}
@layer utilities {
.top-0 {
top: calc(var(--spacing) * 0);
}
.z-10 {
z-index: 10;
}
.min-h-screen {
min-height: 100vh;
}
}
Describe your issue
Maybe I'm the only one in the world using tailwind with slang templates, Slang is a template almost identical to slim written in Crystal.
Using the very same process used to extract CSS classes from slim templates will just work for slang templates, I'm creating this issue because nowadays I need to write my templates like body class="min-h-screen" instead of just body.min-h-screen.
There was an issue for slim templates in the past, so I believe that just let this code also run to slang files (.slang extension) fixes that.
If I knew rust I would not even create this issue but just create a fork of tailwind and fix it on my fork just for myself since I know that the audience is really very little.
That being said, I understand if you guys don't want to fix that because no one uses slang templates, but if so can you guys just say where to tell tailwind to use the same slim/haml heuristics for files with .slang extension?
Thanks very much.
Maybe if I do just need to change crates/oxide/src/scanner/mod.rs
match extension {
"clj" | "cljs" | "cljc" => Clojure.process(content),
"cshtml" | "razor" => Razor.process(content),
"haml" => Haml.process(content),
"json" => Json.process(content),
"pug" => Pug.process(content),
"rb" | "erb" => Ruby.process(content),
"slim" => Slim.process(content),
"svelte" => Svelte.process(content),
"vue" => Vue.process(content),
_ => content.to_vec(),
}
to
match extension {
"clj" | "cljs" | "cljc" => Clojure.process(content),
"cshtml" | "razor" => Razor.process(content),
"haml" => Haml.process(content),
"json" => Json.process(content),
"pug" => Pug.process(content),
"rb" | "erb" => Ruby.process(content),
"slim" | "slang" => Slim.process(content),
"svelte" => Svelte.process(content),
"vue" => Vue.process(content),
_ => content.to_vec(),
}
Nice, I just have no idea how to build this thing π
, cargo build builds something to somewhere... but there's a lot of typescript files... and the Archlinux AUR packages didn't help because they use a JS package as source for the package π’.
Would be nice to have a way to tell tailwind that an extension is an alias to another one, so issue like that could be solved by configuration, not a patches.
@hugopl Hey! Seems like you're on the right track there already, awesome!
Did you do a pnpm install before you tried the cargo build? Usually it's enough to do pnpm test afterwards since that will build a test build of the Rust stuff as well. π
I'm tryign to compile this without my patch but without success.
On v4.1.5 tag I did:
$ pnpm install
$ cargo build -r
$ NODE_ENV=production pnpm run build
Then got:
warning: [email protected]: crt1-reactor.o not found at , the multi-threaded runtime may not be initialized correctly
β error[E0463]: can't find crate for `std`
β |
β = note: the `wasm32-wasip1-threads` target may not be installed
β = help: consider downloading the target with `rustup target add wasm32-wasip1-threads`
β
β For more information about this error, try `rustc --explain E0463`.
β error[E0463]: can't find crate for `core`
β |
β = note: the `wasm32-wasip1-threads` target may not be installed
β = help: consider downloading the target with `rustup target add wasm32-wasip1-threads`
β
β error: could not compile `same-file` (lib) due to 1 previous error
β warning: build failed, waiting for other jobs to finish...
β error: could not compile `once_cell` (lib) due to 1 previous error
β error: could not compile `lazy_static` (lib) due to 1 previous error
β error: could not compile `overload` (lib) due to 1 previous error
β error: could not compile `log` (lib) due to 1 previous error
β error: could not compile `either` (lib) due to 1 previous error
β error: could not compile `bitflags` (lib) due to 1 previous error
β error: could not compile `pin-project-lite` (lib) due to 1 previous error
β error: could not compile `cfg-if` (lib) due to 1 previous error
β error: could not compile `minimal-lexical` (lib) due to 1 previous error
β error: could not compile `memchr` (lib) due to 1 previous error
β error: could not compile `rustc-hash` (lib) due to 1 previous error
β error: could not compile `dunce` (lib) due to 1 previous error
β error: could not compile `crossbeam-utils` (lib) due to 1 previous error
β error: could not compile `smallvec` (lib) due to 1 previous error
β error: could not compile `arrayvec` (lib) due to 1 previous error
β error: could not compile `napi-sys` (lib) due to 1 previous error
β error: could not compile `regex-syntax` (lib) due to 1 previous error
β error: could not compile `regex-syntax` (lib) due to 1 previous error
β Internal Error: Build failed with exit code 101
β at ChildProcess.<anonymous> (file:///home/hugo/src/estudos/tailwindcss/node_modules/.pnpm/@[email protected]_@[email protected]_@[email protected]
β [email protected][email protected]_/node_modules/@napi-rs/cli/dist/api/build.js:199:28)
β at Object.onceWrapper (node:events:639:26)
β at ChildProcess.emit (node:events:536:35)
β at ChildProcess._handle.onexit (node:internal/child_process:293:12)
β βELIFECYCLEβ Command failed with exit code 1.
β βELIFECYCLEβ Command failed with exit code 1.
β command finished with error: command (/home/hugo/src/estudos/tailwindcss/crates/node) /home/hugo/.local/share/pnpm/.tools/pnpm/9.6.0/bin/pnpm run build exited (
β 1)
βββββ>
@tailwindcss/oxide#build: command (/home/hugo/src/estudos/tailwindcss/crates/node) /home/hugo/.local/share/pnpm/.tools/pnpm/9.6.0/bin/pnpm run build exited (1)
Tasks: 0 successful, 1 total
Cached: 0 cached, 1 total
Time: 30.57s
Failed: @tailwindcss/oxide#build
ERROR run failed: command exited (1)
βELIFECYCLEβ Command failed with exit code 1.
When I run the tests some of them fails with:
Error: Failed to resolve entry for package "@tailwindcss/node". The package may have incorrect main/module/exports specified in its package.json.
Try running
$ rustup target add wasm32-wasip1-threads
Before trying the commands again.
@hugopl Working on some updated contribution docs that might be helpful for you: https://github.com/tailwindlabs/tailwindcss/pull/17911
@philipp-spiess trying to debug a similar issue, tried pnpm build on MacOS, got this:
error: failed to run custom build command for `tailwind-oxide v0.0.0 (/private/tmp/tailwindcss/crates/node)`
Caused by:
process didn't exit successfully: `/private/tmp/tailwindcss/target/release/build/tailwind-oxide-87851038cefb8bb9/build-script-build` (exit status: 101)
--- stdout
cargo:rerun-if-env-changed=DEBUG_GENERATED_CODE
cargo:rerun-if-env-changed=TYPE_DEF_TMP_PATH
cargo:rerun-if-env-changed=CARGO_CFG_NAPI_RS_CLI_VERSION
--- stderr
thread 'main' panicked at /Users/iliakan/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/napi-build-2.1.6/src/wasi.rs:4:46:
EMNAPI_LINK_DIR must be set: NotPresent
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Any ideas? π
@iliakan I think you might be missing a pnpm install before the build, could that be? π€ Seems that emnapi is not found which is added here: https://github.com/tailwindlabs/tailwindcss/blob/main/crates/node/package.json#L38
Sadly none worked, and I'm not really interested in spending time learning about rust and JS tools. But I can write a patch with tests and check if it gets green on CI. For a tiny patch like this one this blind development strategy may work.