diff --git a/CHANGELOG.md b/CHANGELOG.md
index f239b0a..9d43f8f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
-- Nothing yet!
+### Added
+
+- Support of `max-width` container queries using `@max` or `atMax` variants.
## [0.1.1] - 2023-03-31
diff --git a/README.md b/README.md
index 0538d51..7ce9230 100644
--- a/README.md
+++ b/README.md
@@ -39,6 +39,26 @@ Start by marking an element as a container using the `@container` class, and the
By default we provide [container sizes](#configuration) from `@xs` (`20rem`) to `@7xl` (`80rem`).
+In case of `max-width` container queries:
+
+```html
+
+```
+
+or alternatively there is an `atMax` version:
+
+```html
+
+```
+
### Named containers
You can optionally name containers using a `@container/{name}` class, and then include that name in the container variants using classes like `@lg/{name}:underline`:
@@ -52,6 +72,26 @@ You can optionally name containers using a `@container/{name}` class, and then i
```
+In case of `max-width` container queries:
+
+```html
+
+```
+
+or alternatively the `atMax` version:
+
+```html
+
+```
+
### Arbitrary container sizes
In addition to using one of the [container sizes](#configuration) provided by default, you can also create one-off sizes using any arbitrary value:
@@ -64,6 +104,56 @@ In addition to using one of the [container sizes](#configuration) provided by de
```
+In case of `max-width` container queries:
+
+```html
+
+```
+
+or alternatively the `atMax` version:
+
+```html
+
+```
+
+### Combining named containers and arbitrary container sizes
+
+You can combine both [named containers](#named-containers) and
+[arbitrary container sizes](#arbitrary-container-sizes) this way:
+
+```html
+
+
+```
+
+In case of `max-width` container queries only the `atMax` version is working
+because to support extraction of `@max-[17.5rem]/main:underline` by the default
+extractor of Tailwind CSS its regular expressions would need to be updated
+(or a custom extractor could be used but that is really an advanced topic since it
+overrides the default one which does really an excellent job to extract class name
+candidates from anywhere).
+
+```html
+
+```
+
+
+
### Removing a container
To stop an element from acting as a container, use the `@container-normal` class.
diff --git a/src/index.ts b/src/index.ts
index 84b9869..37092c6 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -29,6 +29,34 @@ export = plugin(
}
)
+ const sort: (
+ a: { value: string; modifier: string | null },
+ b: { value: string; modifier: string | null }
+ ) => number = (aVariant, zVariant) => {
+ let a = parseFloat(aVariant.value)
+ let z = parseFloat(zVariant.value)
+
+ if (a === null || z === null) return 0
+
+ // Sort values themselves regardless of unit
+ if (a - z !== 0) return a - z
+
+ let aLabel = aVariant.modifier ?? ''
+ let zLabel = zVariant.modifier ?? ''
+
+ // Explicitly move empty labels to the end
+ if (aLabel === '' && zLabel !== '') {
+ return 1
+ } else if (aLabel !== '' && zLabel === '') {
+ return -1
+ }
+
+ // Sort labels alphabetically in the English locale
+ // We are intentionally overriding the locale because we do not want the sort to
+ // be affected by the machine's locale (be it a developer or CI environment)
+ return aLabel.localeCompare(zLabel, 'en', { numeric: true })
+ }
+
matchVariant(
'@',
(value = '', { modifier }) => {
@@ -38,32 +66,34 @@ export = plugin(
},
{
values,
- sort(aVariant, zVariant) {
- let a = parseFloat(aVariant.value)
- let z = parseFloat(zVariant.value)
-
- if (a === null || z === null) return 0
+ sort,
+ }
+ )
- // Sort values themselves regardless of unit
- if (a - z !== 0) return a - z
+ const maxVariantFn: (value: string, { modifier }: { modifier: string | null }) => string | string[] = (value = '', { modifier }) => {
+ let parsed = parseValue(value)
- let aLabel = aVariant.modifier ?? ''
- let zLabel = zVariant.modifier ?? ''
+ return parsed !== null ? `@container ${modifier ?? ''} (max-width: ${value})` : []
+ }
- // Explicitly move empty labels to the end
- if (aLabel === '' && zLabel !== '') {
- return 1
- } else if (aLabel !== '' && zLabel === '') {
- return -1
- }
+ matchVariant(
+ '@max',
+ maxVariantFn,
+ {
+ values,
+ sort,
+ }
+ )
- // Sort labels alphabetically in the English locale
- // We are intentionally overriding the locale because we do not want the sort to
- // be affected by the machine's locale (be it a developer or CI environment)
- return aLabel.localeCompare(zLabel, 'en', { numeric: true })
- },
+ matchVariant(
+ 'atMax',
+ maxVariantFn,
+ {
+ values,
+ sort,
}
)
+
},
{
theme: {
diff --git a/tests/index.test.ts b/tests/index.test.ts
index 077b628..d63db2a 100644
--- a/tests/index.test.ts
+++ b/tests/index.test.ts
@@ -1,5 +1,5 @@
import { expect } from '@jest/globals'
-import { html, css, run } from './run'
+import { css, html, run } from './run'
it('container queries', () => {
let config = {
@@ -245,3 +245,493 @@ it('should be possible to use default container queries', () => {
`)
})
})
+
+it('max-width container queries', () => {
+ let config = {
+ content: [
+ {
+ raw: html`
+
+ `,
+ },
+ ],
+ theme: {
+ containers: {
+ sm: '320px',
+ md: '768px',
+ lg: '1024px',
+ },
+ },
+ corePlugins: { preflight: false },
+ }
+
+ let input = css`
+ @tailwind utilities;
+ `
+
+ return run(input, config).then((result) => {
+ expect(result.css).toMatchFormattedCss(css`
+ .\@container {
+ container-type: inline-size;
+ }
+
+ .\@container-normal {
+ container-type: normal;
+ }
+
+ .\@container\/sidebar {
+ container-type: inline-size;
+ container-name: sidebar;
+ }
+
+ .\@container-normal\/sidebar {
+ container-type: normal;
+ container-name: sidebar;
+ }
+
+ @container (max-width: 123px) {
+ .\@max-\[123px\]\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container (max-width: 200rem) {
+ .\@max-\[200rem\]\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container (max-width: 312px) {
+ .\@max-\[312px\]\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container container1 (max-width: 320px) {
+ .\@max-sm\/container1\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container container2 (max-width: 320px) {
+ .\@max-sm\/container2\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container container10 (max-width: 320px) {
+ .\@max-sm\/container10\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container (max-width: 320px) {
+ .\@max-sm\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container container1 (max-width: 768px) {
+ .\@max-md\/container1\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container container2 (max-width: 768px) {
+ .\@max-md\/container2\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container container10 (max-width: 768px) {
+ .\@max-md\/container10\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container (max-width: 768px) {
+ .\@max-md\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container container1 (max-width: 1024px) {
+ .\@max-lg\/container1\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container container2 (max-width: 1024px) {
+ .\@max-lg\/container2\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container container10 (max-width: 1024px) {
+ .\@max-lg\/container10\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container (max-width: 1024px) {
+ .\@max-lg\:underline {
+ text-decoration-line: underline;
+ }
+ .\@max-\[1024px\]\:underline {
+ text-decoration-line: underline;
+ }
+ }
+ `)
+ })
+})
+
+it('should be possible to use default max-width container queries', () => {
+ let config = {
+ content: [
+ {
+ raw: html`
+
+ `,
+ },
+ ],
+ theme: {},
+ corePlugins: { preflight: false },
+ }
+
+ let input = css`
+ @tailwind utilities;
+ `
+
+ return run(input, config).then((result) => {
+ expect(result.css).toMatchFormattedCss(css`
+ @container (max-width: 20rem) {
+ .\@max-xs\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container (max-width: 24rem) {
+ .\@max-sm\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container (max-width: 28rem) {
+ .\@max-md\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container (max-width: 32rem) {
+ .\@max-lg\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container (max-width: 48rem) {
+ .\@max-3xl\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container (max-width: 64rem) {
+ .\@max-5xl\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container (max-width: 72rem) {
+ .\@max-6xl\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container (max-width: 80rem) {
+ .\@max-7xl\:underline {
+ text-decoration-line: underline;
+ }
+ }
+ `)
+ })
+})
+
+it('atMax max-width container queries', () => {
+ let config = {
+ content: [
+ {
+ raw: html`
+
+ `,
+ },
+ ],
+ theme: {
+ containers: {
+ sm: '320px',
+ md: '768px',
+ lg: '1024px',
+ },
+ },
+ corePlugins: { preflight: false },
+ }
+
+ let input = css`
+ @tailwind utilities;
+ `
+
+ return run(input, config).then((result) => {
+ expect(result.css).toMatchFormattedCss(css`
+ .\@container {
+ container-type: inline-size;
+ }
+
+ .\@container-normal {
+ container-type: normal;
+ }
+
+ .\@container\/sidebar {
+ container-type: inline-size;
+ container-name: sidebar;
+ }
+
+ .\@container-normal\/sidebar {
+ container-type: normal;
+ container-name: sidebar;
+ }
+
+ @container (max-width: 123px) {
+ .atMax-\[123px\]\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container (max-width: 200rem) {
+ .atMax-\[200rem\]\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container (max-width: 312px) {
+ .atMax-\[312px\]\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container container1 (max-width: 320px) {
+ .atMax-sm\/container1\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container container2 (max-width: 320px) {
+ .atMax-sm\/container2\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container container10 (max-width: 320px) {
+ .atMax-sm\/container10\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container (max-width: 320px) {
+ .atMax-sm\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container container1 (max-width: 768px) {
+ .atMax-md\/container1\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container container2 (max-width: 768px) {
+ .atMax-md\/container2\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container container10 (max-width: 768px) {
+ .atMax-md\/container10\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container (max-width: 768px) {
+ .atMax-md\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container container1 (max-width: 1024px) {
+ .atMax-lg\/container1\:underline {
+ text-decoration-line: underline;
+ }
+
+ .atMax-\[1024px\]\/container1\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container container2 (max-width: 1024px) {
+ .atMax-lg\/container2\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container container10 (max-width: 1024px) {
+ .atMax-lg\/container10\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container (max-width: 1024px) {
+ .atMax-lg\:underline {
+ text-decoration-line: underline;
+ }
+ .atMax-\[1024px\]\:underline {
+ text-decoration-line: underline;
+ }
+ }
+ `)
+ })
+})
+
+it('should be possible to use default atMax max-width container queries', () => {
+ let config = {
+ content: [
+ {
+ raw: html`
+
+ `,
+ },
+ ],
+ theme: {},
+ corePlugins: { preflight: false },
+ }
+
+ let input = css`
+ @tailwind utilities;
+ `
+
+ return run(input, config).then((result) => {
+ expect(result.css).toMatchFormattedCss(css`
+ @container (max-width: 20rem) {
+ .atMax-xs\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container (max-width: 24rem) {
+ .atMax-sm\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container (max-width: 28rem) {
+ .atMax-md\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container (max-width: 32rem) {
+ .atMax-lg\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container (max-width: 48rem) {
+ .atMax-3xl\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container (max-width: 64rem) {
+ .atMax-5xl\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container (max-width: 72rem) {
+ .atMax-6xl\:underline {
+ text-decoration-line: underline;
+ }
+ }
+
+ @container (max-width: 80rem) {
+ .atMax-7xl\:underline {
+ text-decoration-line: underline;
+ }
+ }
+ `)
+ })
+})