diff --git a/docs/src/markdown/about/changelog.md b/docs/src/markdown/about/changelog.md
index 37bf302..c4102d4 100644
--- a/docs/src/markdown/about/changelog.md
+++ b/docs/src/markdown/about/changelog.md
@@ -5,6 +5,8 @@
 - **NEW**: Allow `:contans()` to accept a list of text to search for.
 - **FIX**: Don't install test files when installing the `soupsieve` package.
 - **FIX**: Improve efficiency of `:contains()` comparison.
+- **FIX**: Null characters should translate to the Unicode REPLACEMENT CHARACTER (`U+FFFD`) according to the spec. This
+applies to CSS escaped NULL characters as well.
 
 ## 1.8.0
 
diff --git a/soupsieve/css_parser.py b/soupsieve/css_parser.py
index a537d5f..ba16fc9 100644
--- a/soupsieve/css_parser.py
+++ b/soupsieve/css_parser.py
@@ -7,6 +7,8 @@ from . import css_types as ct
 from collections import OrderedDict
 from .util import SelectorSyntaxError
 
+UNICODE_REPLACEMENT_CHAR = 0xFFFD
+
 # Simple pseudo classes that take no parameters
 PSEUDO_SIMPLE = {
     ":any-link",
@@ -240,13 +242,11 @@ def css_unescape(content, string=False):
     def replace(m):
         """Replace with the appropriate substitute."""
 
-        return util.uchr(int(m.group(1)[1:], 16)) if m.group(1) else m.group(2)[1:]
-
-    def replace_string(m):
-        """Replace with the appropriate substitute for a string."""
-
         if m.group(1):
-            value = util.uchr(int(m.group(1)[1:], 16))
+            codepoint = int(m.group(1)[1:], 16)
+            if codepoint == 0:
+                codepoint = UNICODE_REPLACEMENT_CHAR
+            value = util.uchr(codepoint)
         elif m.group(2):
             value = m.group(2)[1:]
         else:
@@ -254,7 +254,7 @@ def css_unescape(content, string=False):
 
         return value
 
-    return RE_CSS_ESC.sub(replace, content) if not string else RE_CSS_STR_ESC.sub(replace_string, content)
+    return (RE_CSS_ESC if not string else RE_CSS_STR_ESC).sub(replace, content)
 
 
 class SelectorPattern(object):
@@ -376,7 +376,7 @@ class CSSParser(object):
     def __init__(self, selector, custom=None, flags=0):
         """Initialize."""
 
-        self.pattern = selector
+        self.pattern = selector.replace('\x00', '\ufffd')
         self.flags = flags
         self.debug = self.flags & util.DEBUG
         self.quirks = self.flags & util._QUIRKS

Null should be replaced with \fffd  REPLACEMENT CHARACTER (�)
This appears to be something that escapes should translate to (`\0`) and also literal NULL. All of the parsers used by BeautifulSoup remove any Null characters from HTML source, so searching for NULL is impossible. On top of that, the CSS spec states that NULL characters should be replaced with \fffd  REPLACEMENT CHARACTER (�). Hex escapes should not translate to NULL, but also translate to `\fffd`  REPLACEMENT CHARACTER (�).
diff --git a/tests/test_level1/test_class.py b/tests/test_level1/test_class.py
index 62ea835..105455a 100644
--- a/tests/test_level1/test_class.py
+++ b/tests/test_level1/test_class.py
@@ -15,6 +15,17 @@ class TestClass(util.TestCase):
     </div>
     """
 
+    # Browsers normally replace NULL with `\uFFFD`, but some of the parsers
+    # we test just strip out NULL, so we will simulate and just insert `\uFFFD` directly
+    # to ensure consistent behavior in our tests across parsers.
+    MARKUP_NULL = """
+    <div>
+    <p>Some text <span id="1" class="\ufffdfoo"> in a paragraph</span>.
+    <a id="2" class="\ufffdbar" href="http://google.com">Link</a>
+    </p>
+    </div>
+    """
+
     def test_class(self):
         """Test class."""
 
@@ -35,6 +46,16 @@ class TestClass(util.TestCase):
             flags=util.HTML
         )
 
+    def test_type_and_class_escaped_null(self):
+        """Test type and class with an escaped null character."""
+
+        self.assert_selector(
+            self.MARKUP_NULL,
+            r"a.\0 bar",
+            ["2"],
+            flags=util.HTML
+        )
+
     def test_malformed_class(self):
         """Test malformed class."""
 
diff --git a/tests/test_level2/test_attribute.py b/tests/test_level2/test_attribute.py
index 5a919f7..1d39ba0 100644
--- a/tests/test_level2/test_attribute.py
+++ b/tests/test_level2/test_attribute.py
@@ -33,6 +33,22 @@ class TestAttribute(util.TestCase):
     </div>
     """
 
+    # Browsers normally replace NULL with `\uFFFD`, but some of the parsers
+    # we test just strip out NULL, so we will simulate and just insert `\uFFFD` directly
+    # to ensure consistent behavior in our tests across parsers.
+    MARKUP_NULL = """
+    <div id="div">
+    <p id="0">Some text <span id="1"> in a paragraph</span>.</p>
+    <a id="2" href="http://google.com">Link</a>
+    <span id="3">Direct child</span>
+    <pre id="\ufffdpre">
+    <span id="4">Child 1</span>
+    <span id="5">Child 2</span>
+    <span id="6">Child 3</span>
+    </pre>
+    </div>
+    """
+
     def test_attribute(self):
         """Test attribute."""
 
@@ -150,6 +166,26 @@ class TestAttribute(util.TestCase):
             flags=util.HTML
         )
 
+    def test_attribute_equal_literal_null(self):
+        """Test attribute with value that equals specified value with a literal null character."""
+
+        self.assert_selector(
+            self.MARKUP_NULL,
+            '[id="\x00pre"]',
+            ["\ufffdpre"],
+            flags=util.HTML
+        )
+
+    def test_attribute_equal_escaped_null(self):
+        """Test attribute with value that equals specified value with an escaped null character."""
+
+        self.assert_selector(
+            self.MARKUP_NULL,
+            r'[id="\0 pre"]',
+            ["\ufffdpre"],
+            flags=util.HTML
+        )
+
     def test_invalid_tag(self):
         """
         Test invalid tag.

pip install -e .
parse_log_pytest
requirements.txt
pytest pytest-cov coverage lxml html5lib mypy
apt-get update apt-get install -y gcc
3.9
requirements/project.txt requirements/tests.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
beautifulsoup4==4.13.3
coverage==7.8.0
exceptiongroup==1.2.2
html5lib==1.1
iniconfig==2.1.0
lxml==5.3.1
mypy==1.15.0
mypy-extensions==1.0.0
packaging==24.2
pluggy==1.5.0
pytest==8.3.5
pytest-cov==6.0.0
six==1.17.0
-e git+https://github.com/facelessuser/soupsieve.git@cba2f470f7ab757d8365c4ab26b2300ea0324542#egg=soupsieve
tomli==2.2.1
typing_extensions==4.13.0
webencodings==0.5.1

name: soupsieve
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - beautifulsoup4==4.13.3
      - coverage==7.8.0
      - exceptiongroup==1.2.2
      - html5lib==1.1
      - iniconfig==2.1.0
      - lxml==5.3.1
      - mypy==1.15.0
      - mypy-extensions==1.0.0
      - packaging==24.2
      - pluggy==1.5.0
      - pytest==8.3.5
      - pytest-cov==6.0.0
      - six==1.17.0
      - tomli==2.2.1
      - typing-extensions==4.13.0
      - webencodings==0.5.1
prefix: /opt/conda/envs/soupsieve

tests/test_level1/test_class.py::TestClass::test_type_and_class_escaped_null tests/test_level1/test_class.py::TestClassQuirks::test_type_and_class_escaped_null tests/test_level2/test_attribute.py::TestAttribute::test_attribute_equal_escaped_null tests/test_level2/test_attribute.py::TestAttribute::test_attribute_equal_literal_null tests/test_level2/test_attribute.py::TestAttributeQuirks::test_attribute_equal_escaped_null tests/test_level2/test_attribute.py::TestAttributeQuirks::test_attribute_equal_literal_null
tests/test_level1/test_class.py::TestClass::test_class tests/test_level1/test_class.py::TestClass::test_class_xhtml tests/test_level1/test_class.py::TestClass::test_malformed_class tests/test_level1/test_class.py::TestClass::test_malformed_pseudo_class tests/test_level1/test_class.py::TestClass::test_multiple_classes tests/test_level1/test_class.py::TestClass::test_type_and_class tests/test_level1/test_class.py::TestClassQuirks::test_class tests/test_level1/test_class.py::TestClassQuirks::test_class_xhtml tests/test_level1/test_class.py::TestClassQuirks::test_malformed_class tests/test_level1/test_class.py::TestClassQuirks::test_malformed_pseudo_class tests/test_level1/test_class.py::TestClassQuirks::test_multiple_classes tests/test_level1/test_class.py::TestClassQuirks::test_type_and_class tests/test_level2/test_attribute.py::TestAttribute::test_attribute tests/test_level2/test_attribute.py::TestAttribute::test_attribute_bad tests/test_level2/test_attribute.py::TestAttribute::test_attribute_contains_cannot_have_empty tests/test_level2/test_attribute.py::TestAttribute::test_attribute_contains_cannot_have_escaped_spaces tests/test_level2/test_attribute.py::TestAttribute::test_attribute_contains_cannot_have_spaces tests/test_level2/test_attribute.py::TestAttribute::test_attribute_contains_space_end tests/test_level2/test_attribute.py::TestAttribute::test_attribute_contains_space_middle tests/test_level2/test_attribute.py::TestAttribute::test_attribute_contains_space_start tests/test_level2/test_attribute.py::TestAttribute::test_attribute_equal_case_insensitive_attribute tests/test_level2/test_attribute.py::TestAttribute::test_attribute_equal_no_quotes tests/test_level2/test_attribute.py::TestAttribute::test_attribute_equal_quotes_and_spaces tests/test_level2/test_attribute.py::TestAttribute::test_attribute_equal_with_double_quotes tests/test_level2/test_attribute.py::TestAttribute::test_attribute_equal_with_quotes tests/test_level2/test_attribute.py::TestAttribute::test_attribute_escaped_newline tests/test_level2/test_attribute.py::TestAttribute::test_attribute_start_dash tests/test_level2/test_attribute.py::TestAttribute::test_attribute_type_html tests/test_level2/test_attribute.py::TestAttribute::test_attribute_type_xhtml tests/test_level2/test_attribute.py::TestAttribute::test_attribute_type_xml tests/test_level2/test_attribute.py::TestAttribute::test_attribute_with_spaces tests/test_level2/test_attribute.py::TestAttribute::test_invalid_tag tests/test_level2/test_attribute.py::TestAttribute::test_malformed_no_quirk tests/test_level2/test_attribute.py::TestAttribute::test_multi_attribute tests/test_level2/test_attribute.py::TestAttributeQuirks::test_attribute tests/test_level2/test_attribute.py::TestAttributeQuirks::test_attribute_bad tests/test_level2/test_attribute.py::TestAttributeQuirks::test_attribute_contains_cannot_have_empty tests/test_level2/test_attribute.py::TestAttributeQuirks::test_attribute_contains_cannot_have_escaped_spaces tests/test_level2/test_attribute.py::TestAttributeQuirks::test_attribute_contains_cannot_have_spaces tests/test_level2/test_attribute.py::TestAttributeQuirks::test_attribute_contains_space_end tests/test_level2/test_attribute.py::TestAttributeQuirks::test_attribute_contains_space_middle tests/test_level2/test_attribute.py::TestAttributeQuirks::test_attribute_contains_space_start tests/test_level2/test_attribute.py::TestAttributeQuirks::test_attribute_equal_case_insensitive_attribute tests/test_level2/test_attribute.py::TestAttributeQuirks::test_attribute_equal_no_quotes tests/test_level2/test_attribute.py::TestAttributeQuirks::test_attribute_equal_quotes_and_spaces tests/test_level2/test_attribute.py::TestAttributeQuirks::test_attribute_equal_with_double_quotes tests/test_level2/test_attribute.py::TestAttributeQuirks::test_attribute_equal_with_quotes tests/test_level2/test_attribute.py::TestAttributeQuirks::test_attribute_escaped_newline tests/test_level2/test_attribute.py::TestAttributeQuirks::test_attribute_quirks tests/test_level2/test_attribute.py::TestAttributeQuirks::test_attribute_start_dash tests/test_level2/test_attribute.py::TestAttributeQuirks::test_attribute_type_html tests/test_level2/test_attribute.py::TestAttributeQuirks::test_attribute_type_xhtml tests/test_level2/test_attribute.py::TestAttributeQuirks::test_attribute_type_xml tests/test_level2/test_attribute.py::TestAttributeQuirks::test_attribute_with_spaces tests/test_level2/test_attribute.py::TestAttributeQuirks::test_invalid_tag tests/test_level2/test_attribute.py::TestAttributeQuirks::test_multi_attribute
docs/src/markdown/about/changelog.md soupsieve/css_parser.py
docs/src/markdown/about/changelog.md soupsieve/css_parser.py