Skip to content

Less Language Extends

SomMeri edited this page Dec 30, 2014 · 54 revisions

If you have two selectors and want them to have exactly the same declarations in exactly the same order on exactly the same css locations, then you have to makes sure that every ruleset with one of them contains also the other one. Less extend keyword allows you to achieve this without having to manually copy any of these two selectors.

Extend allows you to copy selector into every ruleset that contains some other selector. For example, next less sample copies pre:hover selector into every ruleset containing div pre selector:

pre:hover:extend(div pre) {
  // ... body as usually ...
}

Every page element that matches the pre:hover selector gains all declarations assigned to div pre selector. In that sense, extend brings inheritance into css.

Syntax Overview

The extend is either attached to a selector or placed into a ruleset. It looks like a pseudoclass with one or more comma separated selector parameters. Each selector can be followed by optionally keyword all:

  • :extend(selector)
  • :extend(selector all)
  • :extend(selector1 all, selector2)

Attached to Selector

Extend attached to a selector looks like an ordinary pseudoclass with selector as a parameter. A selector can contain multiple extend clauses, but it must end after the last one.

  • Extend after the selector: pre:hover:extend(div pre).
  • Space between selector and extend is allowed: pre:hover :extend(div pre).
  • Multiple extends are allowed: pre:hover:extend(div pre):extend(.bucket tr).
  • This is NOT allowed: pre:hover:extend(div pre).nth-child(odd). Extend must be last.

If a ruleset contains multiple selectors, any of them can have the extend keyword. Multiple selectors with extend in one ruleset:

.big-bucket:extend(.bucket), .big-bag:extend(.bag), .big-division {
  // body
}

Inside Ruleset

Extend can be placed into rulesets body using &:extend(selector) syntax. Placing extend into a body is a shortcut for placing it into every single selector of that ruleset.

Extend inside a body:

pre:hover, .some-class {
  &:extend(div pre);
}

is exactly the same as adding an extend after each selector:

pre:hover:extend(div pre), .some-class:extend(div pre) {
}

Extend Basics

Extend copies selector it is attached to into rulesets containing selector from the argument. If the style sheet contains no media declarations, the selector is copied into every rulesets containing target selector.

Simple Example:

link:hover { // ruleset with target selector 
  color: blue;
}
.some-class:extend(link:hover) { // selector will be copied into rulesets having link:hover 
  margin: 1 2 3 4;
}
link:hover { // another ruleset with target selector 
  padding: 1 1 1 1;
}

Compiled css:

link:hover, .some-class {
  color: blue;
}
.some-class { 
  margin: 1 2 3 4;
}
link:hover, .some-class {
  padding: 1 1 1 1;
}

Extends acts on selectors in compiled css. If it is placed on nested selector, only extending selector is combined with outer selector. Target selector is not combined with it. Following:

.outer {
  div { // compiles into .outer div - will NOT be extended
    color: red;
  }
  .inner:extend(div) { // extend on nested selector
    color: red;
  }
}

compiles into:

.outer div { // target selector lack `all` keyword - nothing was extended
  color: red;
}
.outer .inner { // there is not div selector - nothing was extended
  color: red;
}

Nested Selectors

Extend is able to match nested selectors. Following less:

.bucket {
  tr { // nested ruleset with target selector 
    color: blue;
  }
}
.some-class:extend(.bucket tr) { } // nested ruleset is recognized

is compiled into:

.bucket tr, .some-class {
  color: blue;
}

Exact Matching

Extend looks for exact match between selectors. It does matter whether selector uses leading start or not. It does not matter that two nth-expressions have the same meaning, they need to have to same form in order to be matched. The only exception are quotes in attribute selector, less knows they have to same meaning and matches them.

Leading star does matter. Selectors *.class and .class are equivalent, but extend will not match them:

*.class {
  color: blue;
}
.noStar:extend(.class) {} // this will NOT match the *.class selector

compiles into:

*.class {
  color: blue;
}

Order of pseudoclasses does matter. Selectors link:hover:visited and link:visited:hover match the same set of elements, but extend treats them as different:

link:hover:visited {
  color: blue;
}
.selector:extend(link:visited:hover) {}

compiles into:

link:hover:visited {
  color: blue;
}

Nth expression form does matter. Nth-expressions 1n+3 and n+3 are equivalent, but extend will not match them:

:nth-child(1n+3) {
  color: blue;
}
.child:extend(n+3) {} // 

compiles into:

:nth-child(1n+3) {
  color: blue;
}

Quote type in attribute selector does not matter, all are equivalent. Less file:

[title=identifier] {
  color: blue;
}
[title='identifier'] {
  color: blue;
}
[title="identifier"] {
  color: blue;
}
.noQuote:extend([title=identifier]) {}
.singleQuote:extend([title='identifier']) {}
.doubleQuote:extend([title="identifier"]) {}

compiles into:

[title=identifier], .noQuote, .singleQuote, .doubleQuote {
  color: blue;
}
[title='identifier'], .noQuote, .singleQuote, .doubleQuote { 
  color: blue;
}
[title="identifier"], .noQuote, .singleQuote, .doubleQuote {
  color: blue;
}

Selector Interpolation

Extend is NOT able to match selectors with variables. If selector contains variable, extend will ignore it. However, extend can be attached to interpolated selector.

Selector with variable will not be matched:

@variable: .bucket;
@{variable} { // interpolated selector
  color: blue;
}
.some-class:extend(.bucket) { } // does nothing, match is not found

and extend with variable in target selector matches nothing:

.bucket { 
  color: blue;
}
.some-class:extend(@{variable}) { } // interpolated selector matches nothing
@variable: .bucket;

Two previous examples compile into:

.bucket {
  color: blue;
}

Extend attached to interpolated selector works:

.bucket { 
  color: blue;
}
@{variable}:extend(.bucket) { } 
@variable: .selector;

previous less compiles into:

.bucket, .selector {
  color: blue;
}

Extend All

Extend all replaces parts of selectors. If any part of a selector matches :extend parameter, the extend will add a new selector with replaced matching parts.

Whatever works for :focus should work for :hover too:

pre:focus {
  color: blue
}

:hover:extend(:focus all) {}

compiled css:

pre:focus,
pre:hover {
  color: #0000ff;
}

Matching algorithm looks for complete match between elements names, classes, ids and other selector parts. Extend all will never replace only part of element or class name. The :extend(a all) in following example will not act on caption tag:

caption {
  color: green;
}
.outerClass {
  color: blue;
}
.extending:extend(a all):extend(.outer all) {} // extend will not find a match

compiles into:

caption {
  color: green;
}
.outerClass {
  color: blue;
}

However, if extending selector starts by element name and replaces pseudo-class, the element name will be attached to whatever precedes matched pseudo-class:

input:hover {
  color: green;
}
li:extend(:hover all) {}

compiles into meaningless:

input:hover,
inputli {
  color: green;
}

Extend Inside Media

Extend written inside media declaration should matches only selectors inside that media declaration:

@media print { 
  .screenClass:extend(.selector) {} // extend inside media
  .selector { // this will be matched - it is in the same media
    color: black;
  }
}
.selector { // ruleset on top of style sheet - extend ignores it
  color: red;
}
@media screen { 
  .selector {  // ruleset inside another media - extend ignores it
    color: blue;
  }
}

compiles into:

@media print {
  .selector, .screenClass { // ruleset inside the same media was extended
    color: black;
  }
}
.selector { // ruleset on top of style sheet was ignored
  color: red;
}
@media screen {
  .selector { // ruleset inside another media was ignored
    color: blue;
  }
}

Extend written inside media declaration does not match selectors inside nested declaration:

@media screen { 
  .screenClass:extend(.selector) {} // extend inside media
  @media (min-width: 1023px) {
    .selector {  // ruleset inside nested media - extend ignores it
      color: blue;
    }
  }
}

compiles into:

@media screen and (min-width: 1023px) {
  .selector { // ruleset inside another nested media was ignored
    color: blue;
  }
}

Top level extend matches everything including selectors inside nested media:

@media screen { 
  .selector {  // ruleset inside nested media - top level extend works
    color: blue;
  }
  @media (min-width: 1023px) {
    .selector {  // ruleset inside nested media - top level extend works
      color: blue;
    }
  }
}

.topLevel:extend(.selector) {} // top level extend matches everything

compiles into:

@media screen {
  .selector, .topLevel { // ruleset inside media was extended
    color: blue;
  }
}
@media screen and (min-width: 1023px) {
  .selector, .topLevel { // ruleset inside nested media was extended
    color: blue;
  }
}

Combinations - TODO