mirror of https://github.com/sveltejs/svelte
feat: `container` query support via `css-tree` extension (#8275)
Closes #6969 As discussed there, container query support is quite useful to add to Svelte as it is now broadly available with Firefox releasing support imminently w/ FF v110 this upcoming week (~Feb 14th). Chrome has had support since ~Aug '22. The central issue is that css-tree which is a dependency for CSS AST parsing is significantly lagging behind on adding more recent features such as container query support. Ample time has been given to the maintainer to update css-tree and I do have every confidence that in time css-tree will receive a new major version with all sorts of modern CSS syntax supported including container queries. This PR provides an interim solution for what Svelte needs to support container queries now.pull/8425/head
parent
d49b568019
commit
91e8dfcd6d
@ -0,0 +1,43 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
// Note: Must import from the `css-tree` browser bundled distribution due to `createRequire` usage if importing from
|
||||||
|
// `css-tree` Node module directly. This allows the production build of Svelte to work correctly.
|
||||||
|
import { fork } from '../../../../../node_modules/css-tree/dist/csstree.esm.js';
|
||||||
|
|
||||||
|
import * as Comparison from './node/comparison';
|
||||||
|
import * as ContainerFeature from './node/container_feature';
|
||||||
|
import * as ContainerFeatureRange from './node/container_feature_range';
|
||||||
|
import * as ContainerFeatureStyle from './node/container_feature_style';
|
||||||
|
import * as ContainerQuery from './node/container_query';
|
||||||
|
import * as QueryCSSFunction from './node/query_css_function';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extends `css-tree` for container query support by forking and adding new nodes and at-rule support for `@container`.
|
||||||
|
*
|
||||||
|
* The new nodes are located in `./node`.
|
||||||
|
*/
|
||||||
|
const cqSyntax = fork({
|
||||||
|
atrule: { // extend or override at-rule dictionary
|
||||||
|
container: {
|
||||||
|
parse: {
|
||||||
|
prelude() {
|
||||||
|
return this.createSingleNodeList(
|
||||||
|
this.ContainerQuery()
|
||||||
|
);
|
||||||
|
},
|
||||||
|
block(isStyleBlock = false) {
|
||||||
|
return this.Block(isStyleBlock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
node: { // extend node types
|
||||||
|
Comparison,
|
||||||
|
ContainerFeature,
|
||||||
|
ContainerFeatureRange,
|
||||||
|
ContainerFeatureStyle,
|
||||||
|
ContainerQuery,
|
||||||
|
QueryCSSFunction
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const parse = cqSyntax.parse;
|
@ -0,0 +1,48 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { Delim } from 'css-tree/tokenizer';
|
||||||
|
|
||||||
|
export const name = 'Comparison';
|
||||||
|
export const structure = {
|
||||||
|
value: String
|
||||||
|
};
|
||||||
|
|
||||||
|
export function parse() {
|
||||||
|
const start = this.tokenStart;
|
||||||
|
|
||||||
|
const char1 = this.consume(Delim);
|
||||||
|
|
||||||
|
// The first character in the comparison operator must match '<', '=', or '>'.
|
||||||
|
if (char1 !== '<' && char1 !== '>' && char1 !== '=') {
|
||||||
|
this.error('Malformed comparison operator');
|
||||||
|
}
|
||||||
|
|
||||||
|
let char2;
|
||||||
|
|
||||||
|
if (this.tokenType === Delim) {
|
||||||
|
char2 = this.consume(Delim);
|
||||||
|
|
||||||
|
// The second character in the comparison operator must match '='.
|
||||||
|
if (char2 !== '=') {
|
||||||
|
this.error('Malformed comparison operator');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the next token is also 'Delim' then it is malformed.
|
||||||
|
if (this.tokenType === Delim) {
|
||||||
|
this.error('Malformed comparison operator');
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = char2 ? `${char1}${char2}` : char1;
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'Comparison',
|
||||||
|
loc: this.getLocation(start, this.tokenStart),
|
||||||
|
value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generate(node) {
|
||||||
|
for (let index = 0; index < node.value.length; index++) {
|
||||||
|
this.token(Delim, node.value.charAt(index));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import {
|
||||||
|
Ident,
|
||||||
|
Number,
|
||||||
|
Dimension,
|
||||||
|
Function,
|
||||||
|
LeftParenthesis,
|
||||||
|
RightParenthesis,
|
||||||
|
Colon,
|
||||||
|
Delim
|
||||||
|
} from 'css-tree/tokenizer';
|
||||||
|
|
||||||
|
export const name = 'ContainerFeature';
|
||||||
|
export const structure = {
|
||||||
|
name: String,
|
||||||
|
value: ['Identifier', 'Number', 'Dimension', 'QueryCSSFunction', 'Ratio', null]
|
||||||
|
};
|
||||||
|
|
||||||
|
export function parse() {
|
||||||
|
const start = this.tokenStart;
|
||||||
|
let value = null;
|
||||||
|
|
||||||
|
this.eat(LeftParenthesis);
|
||||||
|
this.skipSC();
|
||||||
|
|
||||||
|
const name = this.consume(Ident);
|
||||||
|
this.skipSC();
|
||||||
|
|
||||||
|
if (this.tokenType !== RightParenthesis) {
|
||||||
|
this.eat(Colon);
|
||||||
|
this.skipSC();
|
||||||
|
|
||||||
|
switch (this.tokenType) {
|
||||||
|
case Number:
|
||||||
|
if (this.lookupNonWSType(1) === Delim) {
|
||||||
|
value = this.Ratio();
|
||||||
|
} else {
|
||||||
|
value = this.Number();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Dimension:
|
||||||
|
value = this.Dimension();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Function:
|
||||||
|
value = this.QueryCSSFunction();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Ident:
|
||||||
|
value = this.Identifier();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
this.error('Number, dimension, ratio, function, or identifier is expected');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.skipSC();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.eat(RightParenthesis);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'ContainerFeature',
|
||||||
|
loc: this.getLocation(start, this.tokenStart),
|
||||||
|
name,
|
||||||
|
value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generate(node) {
|
||||||
|
this.token(LeftParenthesis, '(');
|
||||||
|
this.token(Ident, node.name);
|
||||||
|
|
||||||
|
if (node.value !== null) {
|
||||||
|
this.token(Colon, ':');
|
||||||
|
this.node(node.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.token(RightParenthesis, ')');
|
||||||
|
}
|
@ -0,0 +1,86 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import {
|
||||||
|
Ident,
|
||||||
|
Number,
|
||||||
|
Delim,
|
||||||
|
Dimension,
|
||||||
|
Function,
|
||||||
|
LeftParenthesis,
|
||||||
|
RightParenthesis,
|
||||||
|
WhiteSpace
|
||||||
|
} from 'css-tree/tokenizer';
|
||||||
|
|
||||||
|
export const name = 'ContainerFeatureRange';
|
||||||
|
export const structure = {
|
||||||
|
name: String,
|
||||||
|
value: ['Identifier', 'Number', 'Comparison', 'Dimension', 'QueryCSSFunction', 'Ratio', null]
|
||||||
|
};
|
||||||
|
|
||||||
|
function lookup_non_WS_type_and_value(offset, type, referenceStr) {
|
||||||
|
let current_type;
|
||||||
|
|
||||||
|
do {
|
||||||
|
current_type = this.lookupType(offset++);
|
||||||
|
if (current_type !== WhiteSpace) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} while (current_type !== 0); // NULL -> 0
|
||||||
|
|
||||||
|
return current_type === type ? this.lookupValue(offset - 1, referenceStr) : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parse() {
|
||||||
|
const children = this.createList();
|
||||||
|
let child = null;
|
||||||
|
|
||||||
|
this.eat(LeftParenthesis);
|
||||||
|
this.skipSC();
|
||||||
|
|
||||||
|
while (!this.eof && this.tokenType !== RightParenthesis) {
|
||||||
|
switch (this.tokenType) {
|
||||||
|
case Number:
|
||||||
|
if (lookup_non_WS_type_and_value.call(this, 1, Delim, '/')) {
|
||||||
|
child = this.Ratio();
|
||||||
|
} else {
|
||||||
|
child = this.Number();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Delim:
|
||||||
|
child = this.Comparison();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Dimension:
|
||||||
|
child = this.Dimension();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Function:
|
||||||
|
child = this.QueryCSSFunction();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Ident:
|
||||||
|
child = this.Identifier();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
this.error('Number, dimension, comparison, ratio, function, or identifier is expected');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
children.push(child);
|
||||||
|
|
||||||
|
this.skipSC();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.eat(RightParenthesis);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'ContainerFeatureRange',
|
||||||
|
loc: this.getLocationFromList(children),
|
||||||
|
children
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generate(node) {
|
||||||
|
this.children(node);
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import {
|
||||||
|
Function,
|
||||||
|
Ident,
|
||||||
|
Number,
|
||||||
|
Dimension,
|
||||||
|
RightParenthesis,
|
||||||
|
Colon,
|
||||||
|
Delim
|
||||||
|
} from 'css-tree/tokenizer';
|
||||||
|
|
||||||
|
export const name = 'ContainerFeatureStyle';
|
||||||
|
export const structure = {
|
||||||
|
name: String,
|
||||||
|
value: ['Function', 'Identifier', 'Number', 'Dimension', 'QueryCSSFunction', 'Ratio', null]
|
||||||
|
};
|
||||||
|
|
||||||
|
export function parse() {
|
||||||
|
const start = this.tokenStart;
|
||||||
|
let value = null;
|
||||||
|
|
||||||
|
const function_name = this.consumeFunctionName();
|
||||||
|
if (function_name !== 'style') {
|
||||||
|
this.error('Unknown container style query identifier; "style" is expected');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.skipSC();
|
||||||
|
|
||||||
|
const name = this.consume(Ident);
|
||||||
|
this.skipSC();
|
||||||
|
|
||||||
|
if (this.tokenType !== RightParenthesis) {
|
||||||
|
this.eat(Colon);
|
||||||
|
this.skipSC();
|
||||||
|
|
||||||
|
switch (this.tokenType) {
|
||||||
|
case Number:
|
||||||
|
if (this.lookupNonWSType(1) === Delim) {
|
||||||
|
value = this.Ratio();
|
||||||
|
} else {
|
||||||
|
value = this.Number();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Dimension:
|
||||||
|
value = this.Dimension();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Function:
|
||||||
|
value = this.QueryCSSFunction();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Ident:
|
||||||
|
value = this.Identifier();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
this.error('Number, dimension, ratio, function or identifier is expected');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.skipSC();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.eat(RightParenthesis);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'ContainerFeatureStyle',
|
||||||
|
loc: this.getLocation(start, this.tokenStart),
|
||||||
|
name,
|
||||||
|
value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generate(node) {
|
||||||
|
this.token(Function, 'style(');
|
||||||
|
this.token(Ident, node.name);
|
||||||
|
|
||||||
|
if (node.value !== null) {
|
||||||
|
this.token(Colon, ':');
|
||||||
|
this.node(node.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.token(RightParenthesis, ')');
|
||||||
|
}
|
@ -0,0 +1,130 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import {
|
||||||
|
EOF,
|
||||||
|
WhiteSpace,
|
||||||
|
Comment,
|
||||||
|
Delim,
|
||||||
|
Function,
|
||||||
|
Ident,
|
||||||
|
LeftParenthesis,
|
||||||
|
RightParenthesis,
|
||||||
|
LeftCurlyBracket,
|
||||||
|
Colon
|
||||||
|
} from 'css-tree/tokenizer';
|
||||||
|
|
||||||
|
const CONTAINER_QUERY_KEYWORDS = new Set(['none', 'and', 'not', 'or']);
|
||||||
|
|
||||||
|
export const name = 'ContainerQuery';
|
||||||
|
export const structure = {
|
||||||
|
name: 'Identifier',
|
||||||
|
children: [[
|
||||||
|
'Identifier',
|
||||||
|
'ContainerFeature',
|
||||||
|
'ContainerFeatureRange',
|
||||||
|
'ContainerFeatureStyle',
|
||||||
|
'WhiteSpace'
|
||||||
|
]]
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks ahead to determine if query feature is a range query. This involves locating at least one delimiter and no
|
||||||
|
* colon tokens.
|
||||||
|
*
|
||||||
|
* @returns {boolean} Is potential range query.
|
||||||
|
*/
|
||||||
|
function lookahead_is_range() {
|
||||||
|
let type;
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
let delim_found = false;
|
||||||
|
let no_colon = true;
|
||||||
|
|
||||||
|
// A range query has maximum 5 tokens when formatted as 'mf-range' /
|
||||||
|
// '<mf-value> <mf-lt> <mf-name> <mf-lt> <mf-value>'. So only look ahead maximum of 6 non-whitespace tokens.
|
||||||
|
do {
|
||||||
|
type = this.lookupNonWSType(offset++);
|
||||||
|
if (type !== WhiteSpace) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
if (type === Delim) {
|
||||||
|
delim_found = true;
|
||||||
|
}
|
||||||
|
if (type === Colon) {
|
||||||
|
no_colon = false;
|
||||||
|
}
|
||||||
|
if (type === LeftCurlyBracket || type === RightParenthesis) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} while (type !== EOF && count <= 6);
|
||||||
|
|
||||||
|
return delim_found && no_colon;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parse() {
|
||||||
|
const start = this.tokenStart;
|
||||||
|
const children = this.createList();
|
||||||
|
let child = null;
|
||||||
|
let name = null;
|
||||||
|
|
||||||
|
// Parse potential container name.
|
||||||
|
if (this.tokenType === Ident) {
|
||||||
|
const container_name = this.substring(this.tokenStart, this.tokenEnd);
|
||||||
|
|
||||||
|
// Container name doesn't match a query keyword, so assign it as container name.
|
||||||
|
if (!CONTAINER_QUERY_KEYWORDS.has(container_name.toLowerCase())) {
|
||||||
|
name = container_name;
|
||||||
|
this.eatIdent(container_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.skipSC();
|
||||||
|
|
||||||
|
scan:
|
||||||
|
while (!this.eof) {
|
||||||
|
switch (this.tokenType) {
|
||||||
|
case Comment:
|
||||||
|
case WhiteSpace:
|
||||||
|
this.next();
|
||||||
|
continue;
|
||||||
|
|
||||||
|
case Ident:
|
||||||
|
child = this.Identifier();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Function:
|
||||||
|
child = this.ContainerFeatureStyle();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LeftParenthesis:
|
||||||
|
// Lookahead to determine if range feature.
|
||||||
|
child = lookahead_is_range.call(this) ? this.ContainerFeatureRange() : this.ContainerFeature();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break scan;
|
||||||
|
}
|
||||||
|
|
||||||
|
children.push(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (child === null) {
|
||||||
|
this.error('Identifier or parenthesis is expected');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'ContainerQuery',
|
||||||
|
loc: this.getLocation(start, this.tokenStart - 1),
|
||||||
|
name,
|
||||||
|
children
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generate(node) {
|
||||||
|
if (typeof node.name === 'string') {
|
||||||
|
this.token(Ident, node.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.children(node);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import {
|
||||||
|
RightParenthesis
|
||||||
|
} from 'css-tree/tokenizer';
|
||||||
|
|
||||||
|
const QUERY_CSS_FUNCTIONS = new Set(['calc', 'clamp', 'min', 'max']);
|
||||||
|
|
||||||
|
export const name = 'QueryCSSFunction';
|
||||||
|
export const structure = {
|
||||||
|
name: String,
|
||||||
|
expression: String
|
||||||
|
};
|
||||||
|
|
||||||
|
export function parse() {
|
||||||
|
const start = this.tokenStart;
|
||||||
|
|
||||||
|
const name = this.consumeFunctionName();
|
||||||
|
|
||||||
|
if (!QUERY_CSS_FUNCTIONS.has(name)) {
|
||||||
|
this.error('Unknown query single value function; expected: "calc", "clamp", "max", min"');
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = this.Raw(this.tokenIndex, null, false);
|
||||||
|
|
||||||
|
this.eat(RightParenthesis);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'QueryCSSFunction',
|
||||||
|
loc: this.getLocation(start, this.tokenStart),
|
||||||
|
name,
|
||||||
|
expression: body.value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generate(node) {
|
||||||
|
this.token(Function, `${node.name}(`);
|
||||||
|
|
||||||
|
this.node(node.expression);
|
||||||
|
|
||||||
|
this.token(RightParenthesis, ')');
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
div.svelte-xyz{container:test-container / inline-size}@container (min-width: 400px){div.svelte-xyz{color:red}}@container test-container (min-width: 410px){div.svelte-xyz{color:green}}@container test-container (width < 400px){div.svelte-xyz{color:blue}}@container test-container (0 <= width < 300px){div.svelte-xyz{color:purple}}@container not (width < 400px){div.svelte-xyz{color:pink}}@container (width > 400px) and (height > 400px){div.svelte-xyz{color:lightgreen}}@container (width > 400px) or (height > 400px){div.svelte-xyz{color:lightblue}}@container (width > 400px) and (width > 800px) or (orientation: portrait){div.svelte-xyz{color:salmon}}@container style(color: blue){div.svelte-xyz{color:tan}}@container test-container (min-width: calc(400px + 1px)){div.svelte-xyz{color:green}}@container test-container (width < clamp(200px, 40%, 400px)){div.svelte-xyz{color:blue}}@container test-container (calc(400px + 1px) <= width < calc(500px + 1px)){div.svelte-xyz{color:purple}}@container style(--var: calc(400px + 1px)){div.svelte-xyz{color:sandybrown}}
|
@ -0,0 +1,87 @@
|
|||||||
|
<div>container query</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
container: test-container / inline-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Most common container query statements. */
|
||||||
|
|
||||||
|
@container (min-width: 400px) {
|
||||||
|
div {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@container test-container (min-width: 410px) {
|
||||||
|
div {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@container test-container (width < 400px) {
|
||||||
|
div {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@container test-container (0 <= width < 300px) {
|
||||||
|
div {
|
||||||
|
color: purple;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@container not (width < 400px) {
|
||||||
|
div {
|
||||||
|
color: pink;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@container (width > 400px) and (height > 400px) {
|
||||||
|
div {
|
||||||
|
color: lightgreen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@container (width > 400px) or (height > 400px) {
|
||||||
|
div {
|
||||||
|
color: lightblue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@container (width > 400px) and (width > 800px) or (orientation: portrait) {
|
||||||
|
div {
|
||||||
|
color: salmon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@container style(color: blue) {
|
||||||
|
div {
|
||||||
|
color: tan;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@container test-container (min-width: calc(400px + 1px)) {
|
||||||
|
div {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@container test-container (width < clamp(200px, 40%, 400px)) {
|
||||||
|
div {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@container test-container (calc(400px + 1px) <= width < calc(500px + 1px)) {
|
||||||
|
div {
|
||||||
|
color: purple;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@container style(--var: calc(400px + 1px)) {
|
||||||
|
div {
|
||||||
|
color: sandybrown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
Reference in new issue