diff --git a/src/Symfony/Component/CssSelector/CHANGELOG.md b/src/Symfony/Component/CssSelector/CHANGELOG.md
index d2b7fb1d62acf..2e28394fdf7b9 100644
--- a/src/Symfony/Component/CssSelector/CHANGELOG.md
+++ b/src/Symfony/Component/CssSelector/CHANGELOG.md
@@ -11,6 +11,7 @@ CHANGELOG
 ---
 
  * Add support for `:scope`
+ * Add support for `*:has`
 
 4.4.0
 -----
diff --git a/src/Symfony/Component/CssSelector/Exception/SyntaxErrorException.php b/src/Symfony/Component/CssSelector/Exception/SyntaxErrorException.php
index 52d8259b86789..d54f82ebeb5f3 100644
--- a/src/Symfony/Component/CssSelector/Exception/SyntaxErrorException.php
+++ b/src/Symfony/Component/CssSelector/Exception/SyntaxErrorException.php
@@ -17,7 +17,7 @@
  * ParseException is thrown when a CSS selector syntax is not valid.
  *
  * This component is a port of the Python cssselect library,
- * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
+ * which is copyright Ian Bicking, @see https://github.com/scrapy/cssselect.
  *
  * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
  */
diff --git a/src/Symfony/Component/CssSelector/Node/RelationNode.php b/src/Symfony/Component/CssSelector/Node/RelationNode.php
new file mode 100644
index 0000000000000..7f50501d17472
--- /dev/null
+++ b/src/Symfony/Component/CssSelector/Node/RelationNode.php
@@ -0,0 +1,61 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\CssSelector\Node;
+
+/**
+ * Represents a "<selector>:has(<subselector>)" node.
+ *
+ * This component is a port of the Python cssselect library,
+ * which is copyright Ian Bicking, @see https://github.com/scrapy/cssselect.
+ *
+ * @author Franck Ranaivo-Harisoa <franckranaivo@gmail.com>
+ *
+ * @internal
+ */
+class RelationNode extends AbstractNode
+{
+    private NodeInterface $selector;
+    private NodeInterface $subSelector;
+    private string $combinator;
+
+    public function __construct(NodeInterface $selector, string $combinator, NodeInterface $subSelector)
+    {
+        $this->selector = $selector;
+        $this->combinator = $combinator;
+        $this->subSelector = $subSelector;
+    }
+
+    public function getSelector(): NodeInterface
+    {
+        return $this->selector;
+    }
+
+    public function getCombinator(): string
+    {
+        return $this->combinator;
+    }
+
+    public function getSubSelector(): NodeInterface
+    {
+        return $this->subSelector;
+    }
+
+    public function getSpecificity(): Specificity
+    {
+        return $this->selector->getSpecificity()->plus($this->subSelector->getSpecificity());
+    }
+
+    public function __toString(): string
+    {
+        return sprintf('%s[%s:has(%s)]', $this->getNodeName(), $this->selector, $this->subSelector);
+    }
+}
diff --git a/src/Symfony/Component/CssSelector/Parser/Parser.php b/src/Symfony/Component/CssSelector/Parser/Parser.php
index f7eea2f828fc3..7c4eff8c5f0c4 100644
--- a/src/Symfony/Component/CssSelector/Parser/Parser.php
+++ b/src/Symfony/Component/CssSelector/Parser/Parser.php
@@ -11,6 +11,7 @@
 
 namespace Symfony\Component\CssSelector\Parser;
 
+use Symfony\Component\CssSelector\Exception\InternalErrorException;
 use Symfony\Component\CssSelector\Exception\SyntaxErrorException;
 use Symfony\Component\CssSelector\Node;
 use Symfony\Component\CssSelector\Parser\Tokenizer\Tokenizer;
@@ -144,10 +145,42 @@ private function parserSelectorNode(TokenStream $stream, bool $isArgument = fals
         return new Node\SelectorNode($result, $pseudoElement);
     }
 
+    /**
+     * @throws InternalErrorException
+     * @throws SyntaxErrorException
+     */
+    function parseRelativeSelector(TokenStream $stream): array
+    {
+        $stream->skipWhitespace();
+        $subSelector = '';
+        $next = $stream->getNext();
+
+        if ($next->isDelimiter(['-', '+', '>', '~'])) {
+            $combinator = $next->getValue();
+            $stream->skipWhitespace();
+            $next = $stream->getNext();
+        } else {
+            $combinator = new Token(Token::TYPE_DELIMITER, ' ', 0);
+        }
+
+        while(true){
+            if ($next->isString() || $next->isIdentifier() || $next->isNumber()
+                || $next->isDelimiter(['.', '*'])) {
+                $subSelector .= $next->getValue();
+            } elseif ($next->isDelimiter([')'])) {
+                $result = $this->parse($subSelector);
+                return [$combinator, $result[0]];
+            } else {
+                throw SyntaxErrorException::unexpectedToken('an argument', $next);
+            }
+            $next = $stream->getNext();
+        }
+    }
+
     /**
      * Parses next simple node (hash, class, pseudo, negation).
      *
-     * @throws SyntaxErrorException
+     * @throws SyntaxErrorException|InternalErrorException
      */
     private function parseSimpleSelector(TokenStream $stream, bool $insideNegation = false, bool $isArgument = false): array
     {
@@ -253,6 +286,9 @@ private function parseSimpleSelector(TokenStream $stream, bool $insideNegation =
                     }
 
                     $result = new Node\SpecificityAdjustmentNode($result, $selectors);
+                } elseif('has' === strtolower($identifier)) {
+                    [$combinator, $arguments] = $this->parseRelativeSelector($stream);
+                    $result = new Node\RelationNode($result, $combinator ,$arguments);
                 } else {
                     $arguments = [];
                     $next = null;
diff --git a/src/Symfony/Component/CssSelector/Tests/Parser/ParserTest.php b/src/Symfony/Component/CssSelector/Tests/Parser/ParserTest.php
index 82de5ab6b8562..c309484b15115 100644
--- a/src/Symfony/Component/CssSelector/Tests/Parser/ParserTest.php
+++ b/src/Symfony/Component/CssSelector/Tests/Parser/ParserTest.php
@@ -134,6 +134,7 @@ public static function getParserTestData()
             ['div:contains("foo")', ["Function[Element[div]:contains(['foo'])]"]],
             ['div#foobar', ['Hash[Element[div]#foobar]']],
             ['div:not(div.foo)', ['Negation[Element[div]:not(Class[Element[div].foo])]']],
+            ['div:has(div.foo)', ['Relation[Element[div]:has(Selector[Class[Element[div].foo]])]']],
             ['td ~ th', ['CombinedSelector[Element[td] ~ Element[th]]']],
             ['.foo[data-bar][data-baz=0]', ["Attribute[Attribute[Class[Element[*].foo][data-bar]][data-baz = '0']]"]],
             ['div#foo\.bar', ['Hash[Element[div]#foo.bar]']],
diff --git a/src/Symfony/Component/CssSelector/Tests/XPath/TranslatorTest.php b/src/Symfony/Component/CssSelector/Tests/XPath/TranslatorTest.php
index f521a94708423..8b255db90a205 100644
--- a/src/Symfony/Component/CssSelector/Tests/XPath/TranslatorTest.php
+++ b/src/Symfony/Component/CssSelector/Tests/XPath/TranslatorTest.php
@@ -235,6 +235,10 @@ public static function getCssToXPathTestData()
             [':scope', '*[1]'],
             ['e:is(section, article) h1', "e[(name() = 'section') or (name() = 'article')]/descendant-or-self::*/h1"],
             ['e:where(section, article) h1', "e[(name() = 'section') or (name() = 'article')]/descendant-or-self::*/h1"],
+            ['div:has(> .foo)', "div[./*[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]]"],
+            ['div:has(~ .foo)', "div[following-sibling::*[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]]"],
+            ['div:has(+ .foo)', "div[following-sibling::*[(@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')) and (position() = 1)]]"],
+            ['div:has(+ .foo)', "div[following-sibling::*[(@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')) and (position() = 1)]]"],
         ];
     }
 
diff --git a/src/Symfony/Component/CssSelector/XPath/Extension/AbstractExtension.php b/src/Symfony/Component/CssSelector/XPath/Extension/AbstractExtension.php
index 495f882910d5a..30eedb8921eb5 100644
--- a/src/Symfony/Component/CssSelector/XPath/Extension/AbstractExtension.php
+++ b/src/Symfony/Component/CssSelector/XPath/Extension/AbstractExtension.php
@@ -15,7 +15,7 @@
  * XPath expression translator abstract extension.
  *
  * This component is a port of the Python cssselect library,
- * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
+ * which is copyright Ian Bicking, @see https://github.com/scrapy/cssselect.
  *
  * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
  *
@@ -47,4 +47,9 @@ public function getAttributeMatchingTranslators(): array
     {
         return [];
     }
+
+    public function getRelativeCombinationTranslators(): array
+    {
+        return [];
+    }
 }
diff --git a/src/Symfony/Component/CssSelector/XPath/Extension/ExtensionInterface.php b/src/Symfony/Component/CssSelector/XPath/Extension/ExtensionInterface.php
index 1a74b90acc6c3..1d53eba527f23 100644
--- a/src/Symfony/Component/CssSelector/XPath/Extension/ExtensionInterface.php
+++ b/src/Symfony/Component/CssSelector/XPath/Extension/ExtensionInterface.php
@@ -15,10 +15,12 @@
  * XPath expression translator extension interface.
  *
  * This component is a port of the Python cssselect library,
- * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
+ * which is copyright Ian Bicking, @see https://github.com/scrapy/cssselect.
  *
  * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
  *
+ * @method array<string, callable(XPathExpr, XPathExpr): XPathExpr> getRelativeCombinationTranslators() Returns combination translators found inside ":has()" relation.
+ *
  * @internal
  */
 interface ExtensionInterface
diff --git a/src/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php b/src/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php
index 4cd46fa1fc783..4f6f961cad9c0 100644
--- a/src/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php
+++ b/src/Symfony/Component/CssSelector/XPath/Extension/NodeExtension.php
@@ -19,7 +19,7 @@
  * XPath expression translator node extension.
  *
  * This component is a port of the Python cssselect library,
- * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
+ * which is copyright Ian Bicking, @see https://github.com/scrapy/cssselect.
  *
  * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
  *
@@ -71,6 +71,7 @@ public function getNodeTranslators(): array
             'Class' => $this->translateClass(...),
             'Hash' => $this->translateHash(...),
             'Element' => $this->translateElement(...),
+            'Relation' => $this->translateRelation(...),
         ];
     }
 
@@ -209,6 +210,13 @@ public function translateElement(Node\ElementNode $node): XPathExpr
         return $xpath;
     }
 
+    public function translateRelation(Node\RelationNode $node, Translator $translator): XPathExpr
+    {
+        $combinator = $node->getCombinator();
+
+        return $translator->addRelativeCombination($combinator, $node->getSelector(), $node->getSubSelector());
+    }
+
     public function getName(): string
     {
         return 'node';
diff --git a/src/Symfony/Component/CssSelector/XPath/Extension/RelationExtension.php b/src/Symfony/Component/CssSelector/XPath/Extension/RelationExtension.php
new file mode 100644
index 0000000000000..d85ec21dfa069
--- /dev/null
+++ b/src/Symfony/Component/CssSelector/XPath/Extension/RelationExtension.php
@@ -0,0 +1,67 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\CssSelector\XPath\Extension;
+
+use Symfony\Component\CssSelector\XPath\XPathExpr;
+
+/**
+ * XPath expression translator combination extension.
+ *
+ * This component is a port of the Python cssselect library,
+ * which is copyright Ian Bicking, @see https://github.com/scrapy/cssselect.
+ *
+ * @author Franck Ranaivo-Harisoa <franckranaivo@gmail.com>
+ *
+ * @internal
+ */
+class RelationExtension extends AbstractExtension
+{
+    public function getRelativeCombinationTranslators(): array
+    {
+        return [
+            ' ' => $this->translateRelationDescendant(...),
+            '>' => $this->translateRelationChild(...),
+            '+' => $this->translateRelationDirectAdjacent(...),
+            '~' => $this->translateRelationIndirectAdjacent(...),
+        ];
+    }
+
+    public function translateRelationDescendant(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr
+    {
+        return $xpath->join('[descendant-or-self::', $combinedXpath, ']', true);
+    }
+
+    public function translateRelationChild(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr
+    {
+        return $xpath->join('[./', $combinedXpath, ']', true);
+    }
+
+    public function translateRelationDirectAdjacent(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr
+    {
+        $combinedXpath
+            ->addNameTest()
+            ->addCondition('position() = 1');
+
+        return $xpath
+            ->join('[following-sibling::', $combinedXpath, ']', true);
+    }
+
+    public function translateRelationIndirectAdjacent(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr
+    {
+        return $xpath->join('[following-sibling::', $combinedXpath, ']', true);
+    }
+
+    public function getName(): string
+    {
+        return 'relation';
+    }
+}
diff --git a/src/Symfony/Component/CssSelector/XPath/Translator.php b/src/Symfony/Component/CssSelector/XPath/Translator.php
index b2623e5067ed4..d2670a35c7589 100644
--- a/src/Symfony/Component/CssSelector/XPath/Translator.php
+++ b/src/Symfony/Component/CssSelector/XPath/Translator.php
@@ -44,6 +44,7 @@ class Translator implements TranslatorInterface
 
     private array $nodeTranslators = [];
     private array $combinationTranslators = [];
+    private array $relativeCombinationTranslators = [];
     private array $functionTranslators = [];
     private array $pseudoClassTranslators = [];
     private array $attributeMatchingTranslators = [];
@@ -58,6 +59,7 @@ public function __construct(?ParserInterface $parser = null)
             ->registerExtension(new Extension\FunctionExtension())
             ->registerExtension(new Extension\PseudoClassExtension())
             ->registerExtension(new Extension\AttributeMatchingExtension())
+            ->registerExtension(new Extension\RelationExtension())
         ;
     }
 
@@ -120,6 +122,7 @@ public function registerExtension(Extension\ExtensionInterface $extension): stat
         $this->functionTranslators = array_merge($this->functionTranslators, $extension->getFunctionTranslators());
         $this->pseudoClassTranslators = array_merge($this->pseudoClassTranslators, $extension->getPseudoClassTranslators());
         $this->attributeMatchingTranslators = array_merge($this->attributeMatchingTranslators, $extension->getAttributeMatchingTranslators());
+        $this->relativeCombinationTranslators = array_merge($this->relativeCombinationTranslators, $extension->getRelativeCombinationTranslators());
 
         return $this;
     }
@@ -170,6 +173,18 @@ public function addCombination(string $combiner, NodeInterface $xpath, NodeInter
         return $this->combinationTranslators[$combiner]($this->nodeToXPath($xpath), $this->nodeToXPath($combinedXpath));
     }
 
+    /**
+     * @throws ExpressionErrorException
+     */
+    public function addRelativeCombination(string $combiner, NodeInterface $xpath, NodeInterface $combinedXpath): XPathExpr
+    {
+        if (!isset($this->relativeCombinationTranslators[$combiner])) {
+            throw new ExpressionErrorException(sprintf('Combiner "%s" not supported.', $combiner));
+        }
+
+        return $this->relativeCombinationTranslators[$combiner]($this->nodeToXPath($xpath), $this->nodeToXPath($combinedXpath));
+    }
+
     /**
      * @throws ExpressionErrorException
      */
diff --git a/src/Symfony/Component/CssSelector/XPath/XPathExpr.php b/src/Symfony/Component/CssSelector/XPath/XPathExpr.php
index a148febc53e0d..c3144b3f98ebd 100644
--- a/src/Symfony/Component/CssSelector/XPath/XPathExpr.php
+++ b/src/Symfony/Component/CssSelector/XPath/XPathExpr.php
@@ -82,7 +82,7 @@ public function addStarPrefix(): static
      *
      * @return $this
      */
-    public function join(string $combiner, self $expr): static
+    public function join(string $combiner, self $expr, string $closingCombiner = null, bool $hasInnerConditions = false): static
     {
         $path = $this->__toString().$combiner;
 
@@ -91,8 +91,19 @@ public function join(string $combiner, self $expr): static
         }
 
         $this->path = $path;
-        $this->element = $expr->element;
-        $this->condition = $expr->condition;
+
+        if (!$hasInnerConditions) {
+            $this->element = $expr->element.($closingCombiner ?? '');
+            $this->condition = $expr->condition;
+        } else {
+            $this->element = $expr->element;
+            if ($expr->condition) {
+                $this->element .= '['.$expr->condition.']';
+            }
+            if ($closingCombiner) {
+                $this->element .= $closingCombiner;
+            }
+        }
 
         return $this;
     }