npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ivanalbizu/astro-contrast

v0.2.1

Published

WCAG color contrast analyzer for Astro components

Downloads

30

Readme

astro-contrast

WCAG color contrast analyzer for Astro components. Checks your .astro files for color contrast issues at build time, dev time, or on demand via CLI.

Features

  • Parses .astro files and extracts color pairs (text color + background)
  • Calculates WCAG 2.1 contrast ratios
  • Supports hex, rgb, hsl, oklch, oklab, lab, lch, and named CSS colors (including alpha channels)
  • color-mix() support — resolves color-mix() across 7 color spaces
  • Large text detection — applies lower WCAG thresholds for headings, large fonts, and bold text
  • Tailwind CSS support — resolves text-* and bg-* utility classes
  • Resolves CSS custom properties (var(--color)) from :root
  • Auto-detects external CSS<link rel="stylesheet"> and @import are loaded automatically
  • Reads external design tokens (Style Dictionary, Cobalt UI, Terrazzo)
  • Ignore specific elements or rules with comments
  • GitHub Actions annotations — contrast failures appear inline in PR diffs automatically
  • Dev dashboard — visual contrast report in the browser during development
  • Works as CLI, Astro integration, or programmatic API
  • Watch mode for real-time analysis during development

Install

npm install -D @ivanalbizu/astro-contrast

Usage

CLI

# Analyze all .astro files
astro-contrast "src/**/*.astro"

# With WCAG AAA level
astro-contrast "src/**/*.astro" --level aaa

# Show all pairs, not just failures
astro-contrast "src/**/*.astro" --verbose

# Watch mode
astro-contrast "src/**/*.astro" --watch

# With design tokens
astro-contrast "src/**/*.astro" --tokens tokens/colors.json
astro-contrast "src/**/*.astro" --tokens primitives.json --tokens semantic.json

# With external/global CSS
astro-contrast "src/**/*.astro" --css src/styles/global.css
astro-contrast "src/**/*.astro" --css variables.css --css theme.css

# JSON output
astro-contrast "src/**/*.astro" --json

CLI Options

| Option | Description | |---|---| | --level aa\|aaa | WCAG level to check (default: aa) | | --verbose, -v | Show all pairs, not just failures | | --watch, -w | Watch for file changes and re-analyze | | --tokens <file> | Token file (JSON/YAML/CSS). Repeatable | | --css <file> | External CSS file. Repeatable | | --ignore-color <val> | Color to ignore globally. Repeatable | | --ignore-pair <val> | Color pair to ignore (fg:bg). Repeatable | | --ignore-selector <sel> | Selector to ignore (.class, #id, tag, .prefix-*). Repeatable | | --json | Output results as JSON | | --help, -h | Show help | | --version | Show version |

Exit codes

| Code | Meaning | |---|---| | 0 | All pairs pass the required level | | 1 | At least one pair fails | | 2 | Error during analysis |

Astro Integration

Add @ivanalbizu/astro-contrast to your astro.config.mjs:

import { defineConfig } from 'astro/config';
import astroContrast from '@ivanalbizu/astro-contrast';

export default defineConfig({
  integrations: [
    astroContrast({
      level: 'aa',
      verbose: true,
      css: ['src/styles/global.css'],
    }),
  ],
});

This runs contrast analysis automatically during astro dev (with file watching) and astro build.

Integration Options

| Option | Type | Default | Description | |---|---|---|---| | level | 'aa' \| 'aaa' | 'aa' | WCAG level to check | | verbose | boolean | false | Show all pairs, not just failures | | failOnError | boolean | false | Fail the build if contrast issues are found | | tokens | string[] | [] | Paths to token files | | css | string[] | [] | Paths to external CSS files | | ignore | IgnoreConfig | undefined | Global ignore rules (see below) | | dashboard | boolean \| string | false | Dev dashboard URL. true = /_contrast, string = custom path |

Dev Dashboard

Enable the dashboard to view contrast results in the browser during development:

astroContrast({
  dashboard: true, // available at http://localhost:4321/_contrast
})

Or with a custom URL:

astroContrast({
  dashboard: '/debug-astro-contrast',
})

The dashboard shows all analyzed files with their color pairs, contrast ratios, and pass/fail status. It auto-refreshes every 2 seconds when you edit .astro files — no need to reload the page.

A JSON API is also available at {dashboardUrl}/api for programmatic access.

Programmatic API

import { analyzeFile, analyzeFiles } from '@ivanalbizu/astro-contrast';

const result = await analyzeFile('src/components/Button.astro');

console.log(result.stats);
// { elementsAnalyzed: 2, pairsChecked: 2, passing: 1, aaFailing: 1, ... }

for (const cr of result.results) {
  console.log(`${cr.element.tagName} — ${cr.ratio}:1 — ${cr.meetsAA ? 'PASS' : 'FAIL'}`);
}

With external tokens:

import { analyzeFiles } from '@ivanalbizu/astro-contrast';
import { readTokenFiles } from 'astro-contrast/dist/chunk-HPYGY3PW.js'; // internal

const tokens = await readTokenFiles(['tokens/colors.json']);
const results = await analyzeFiles(files, { externalTokens: tokens });

Design Tokens

astro-contrast can read external design token files and resolve var() references against them. Supported formats:

JSON — W3C DTCG / Style Dictionary

Compatible with W3C Design Token Community Group format, used by Cobalt UI, Terrazzo, and Style Dictionary v4:

{
  "color": {
    "$type": "color",
    "primary": {
      "$value": "#1a5276"
    },
    "danger": {
      "$value": "#e74c3c"
    },
    "info": {
      "$value": "{color.primary}"
    }
  }
}

Also supports Style Dictionary v3 format (value/type without $ prefix).

YAML

Same structure as JSON, with .yaml or .yml extension:

color:
  $type: color
  primary:
    $value: "#1a5276"
  danger:
    $value: "#e74c3c"
  info:
    $value: "{color.primary}"

CSS

Plain CSS files with :root declarations:

:root {
  --color-primary: #1a5276;
  --color-danger: #e74c3c;
}

How tokens are mapped

Token paths are converted to CSS custom properties:

| Token path | CSS variable | |---|---| | color.primary | --color-primary | | color.primitives.blue.500 | --color-primitives-blue-500 |

Token references ({color.primary}) are resolved recursively before mapping.

When a .astro file defines the same custom property in its <style> block, the in-file value takes priority over the external token.

External / Global CSS

Auto-detection

astro-contrast automatically detects and loads external CSS referenced in your .astro files via:

  • import "./styles.css" — in the frontmatter (component script)
  • <link rel="stylesheet" href="./styles.css"> — in the HTML template
  • @import './tokens.css' — inside <style> blocks

This works recursively: if a.css imports b.css which imports c.css, all three are loaded. Circular imports are handled safely.

Only relative paths (./, ../) are resolved. Absolute URLs (http://, https://) and package imports are skipped.

Manual --css flag

For CSS files that aren't referenced directly in .astro files (e.g. injected by a build tool), use --css (CLI) or css (integration):

astro-contrast "src/**/*.astro" --css src/styles/global.css --css src/styles/variables.css

External CSS files provide:

  • CSS rules — selectors and declarations (e.g. .heading { color: #1a5276; })
  • Custom properties:root variables (e.g. --color-primary: #1a5276)

Priority order: external tokens < auto-detected CSS < manual --css < in-file :root / <style>. In-file styles always win at equal specificity.

Tailwind CSS

astro-contrast detects Tailwind utility classes and resolves them to colors using the default Tailwind v3 palette. No configuration needed.

Supported patterns:

<!-- Standard palette colors -->
<button class="text-white bg-blue-700">Submit</button>

<!-- Arbitrary values -->
<p class="text-[#1a5276] bg-[#d4e6f1]">Custom colors</p>

| Pattern | Example | Resolves to | |---|---|---| | text-{color}-{shade} | text-blue-500 | color: #3b82f6 | | bg-{color}-{shade} | bg-red-600 | background-color: #dc2626 | | text-white / text-black | text-white | color: #ffffff | | text-[value] | text-[#1a5276] | color: #1a5276 | | bg-[value] | bg-[rgb(26,82,118)] | background-color: rgb(26,82,118) |

Priority order: inline styles > Tailwind classes > CSS rules > defaults.

Not yet supported: responsive variants (md:text-white), state variants (hover:bg-blue-500), opacity modifiers (text-blue-500/50), custom Tailwind config.

color-mix()

astro-contrast resolves CSS color-mix() functions and calculates the resulting color for contrast analysis. Works with var() references inside color-mix().

:root {
  --color-base: #ffffff;
  --color-main: #000000;
  --surface: color-mix(in srgb, var(--color-base) 98%, var(--color-main));
}

Supported color spaces:

| Color space | Interpolation method | |---|---| | srgb | Linear interpolation in gamma-encoded sRGB | | srgb-linear | Linear interpolation in linear-light sRGB | | oklab | Linear interpolation in OKLab (perceptually uniform) | | oklch | Polar interpolation with shortest hue path | | lab | Linear interpolation in CIE Lab | | lch | Polar interpolation with shortest hue path | | hsl | Polar interpolation with shortest hue path |

Percentage arguments follow the CSS spec: both explicit (color-mix(in srgb, red 30%, blue 70%)), single (color-mix(in srgb, red 25%, blue) — blue gets 75%), and omitted (color-mix(in srgb, red, blue) — 50/50).

Ignoring Elements

You can skip specific elements or CSS rules from contrast analysis using ignore comments.

HTML comment

Place <!-- astro-contrast-ignore --> before the element to skip:

<p class="decorative">This is checked</p>
<!-- astro-contrast-ignore -->
<p class="decorative">This is skipped</p>

HTML attribute

Add data-contrast-ignore to the element:

<span class="badge" data-contrast-ignore>Decorative badge</span>

CSS comment

Place /* astro-contrast-ignore */ before a CSS rule to skip it:

.alert {
  color: #fff;
  background-color: #e74c3c;
}
/* astro-contrast-ignore */
.decorative {
  color: #ccc;
  background-color: #ddd;
}

Global Ignore Rules

For repetitive patterns like brand colors that intentionally fail contrast, use global ignore rules instead of per-element comments.

Astro Integration

astroContrast({
  ignore: {
    colors: ['#e74c3c'],                                    // ignore this color everywhere
    pairs: [{ foreground: '#ffffff', background: '#e74c3c' }], // ignore this exact pair
    selectors: ['.brand-badge', '.alert-*'],                // ignore matching elements
  }
})

CLI

# Ignore a color globally (repeatable)
astro-contrast "src/**/*.astro" --ignore-color "#e74c3c"

# Ignore a specific foreground:background pair (repeatable)
astro-contrast "src/**/*.astro" --ignore-pair "#ffffff:#e74c3c"

# Ignore elements by selector, supports * wildcards (repeatable)
astro-contrast "src/**/*.astro" --ignore-selector ".brand-badge" --ignore-selector ".alert-*"

| Rule type | What it does | |---|---| | colors | Ignores any pair where the color appears as foreground or background | | pairs | Ignores only when both foreground and background match exactly | | selectors | Ignores elements matching .class, #id, tag, or wildcard patterns like .prefix-* |

Colors are compared by their resolved RGB value — #e74c3c and rgb(231, 76, 60) are treated as the same color.

CI / GitHub Actions

When running in GitHub Actions, astro-contrast automatically emits annotations that appear inline in pull request diffs. No configuration needed — it detects the GITHUB_ACTIONS environment variable.

Contrast failures appear as error annotations on the exact line of the failing element.

Example workflow:

- name: Check contrast
  run: astro-contrast "src/**/*.astro"

If the check fails (exit code 1), the PR will show annotations like:

Error src/components/Card.astro#L7 Contrast 2.8:1 fails AA (requires 4.5:1) — #999999 on #ffffff (.card-meta)

When using --level aaa, pairs that pass AA but fail AAA appear as warnings instead of errors.

How It Works

  1. Parse — Reads .astro files using @astrojs/compiler and extracts HTML elements, <style> blocks, <link> hrefs, and @import references
  2. Extract CSS — Parses style blocks with PostCSS to get selectors, color declarations, and :root custom properties. Auto-loads CSS from <link> and @import recursively
  3. Resolve — Resolves var() references using custom properties from :root, auto-detected CSS, external CSS files, and/or design tokens. Evaluates color-mix() functions
  4. Match — Matches HTML elements to CSS rules by selector (type, class, ID, compound, descendant, child combinator) and resolves Tailwind utility classes
  5. Evaluate — Calculates WCAG 2.1 contrast ratio for each foreground/background pair
  6. Report — Outputs results with pass/fail status for AA and AAA levels

WCAG 2.1 Contrast Requirements

| Level | Normal text | Large text | |---|---|---| | AA | 4.5:1 | 3:1 | | AAA | 7:1 | 4.5:1 |

Large text is defined as ≥ 18px, or ≥ 14px bold (font-weight ≥ 700). astro-contrast detects font size from inline styles, CSS rules, Tailwind classes (text-xl, font-bold, etc.), and HTML heading defaults (<h1><h4>).

Viewport-dependent values (vw, vh) and CSS functions (clamp(), min(), max(), calc()) cannot be resolved statically. When the font size cannot be determined, astro-contrast defaults to normal text thresholds — the stricter requirement — to avoid false passes.

Example Output

src/components/atoms/Button.astro
   PASS  .btn-primary  L5  #ffffff on #1a5276  →  8.4:1  (AA ✓  AAA ✓)
   PASS  .btn-danger   L6  #ffffff on #514f4f  →  8.1:1  (AA ✓  AAA ✓)

src/components/molecules/Card.astro
   PASS  .card-title  L6  #1a1a1a on #ffffff  →  17.4:1  (AA ✓  AAA ✓) [large]
   FAIL  .card-meta   L7  #999999 on #ffffff  →  2.8:1   (AA requires 4.5:1)
   PASS  .card-body   L8  #333333 on #ffffff  →  12.6:1  (AA ✓  AAA ✓)

Summary:
  Files analyzed: 2
  Color pairs checked: 5
  Passing: 4 | Failing: 1

Development Scripts

| Script | Description | |---|---| | npm run build | Build with tsup | | npm run dev | Build in watch mode | | npm test | Run tests | | npm run test:watch | Tests in watch mode | | npm run typecheck | TypeScript check (tsc --noEmit) | | npm run prepublishOnly | Runs typecheck + test + build (auto on npm publish) |

Alpha Compositing

When a color has an alpha channel (transparency), the visible color depends on what's behind it. astro-contrast composites both foreground and background colors using the standard alpha compositing formula:

composited = color × alpha + behind × (1 - alpha)

Foreground alpha — the text color is composited onto the background:

<p style="color: rgba(0, 0, 0, 0.5); background-color: #ffffff">Semi-transparent text</p>
<!-- Visible text color: rgb(128, 128, 128) → contrast ~3.95:1 instead of 21:1 -->

Background alpha — the background is composited onto the surface behind it (ancestor background → root background → white):

<p style="color: #000; background-color: rgba(255, 0, 0, 0.5)">Text on semi-transparent red</p>
<!-- Background composited on white: rgb(255, 128, 128) — light pink -->

What's supported:

| Case | Example | |---|---| | Foreground rgba()/hsla()/hex8 on opaque background | rgba(0,0,0,0.5) on #fff | | Background rgba()/hsla()/hex8 composited on ancestor/root/white | bg: rgba(255,0,0,0.5) on implicit white |

Not yet supported:

| Case | Why | |---|---| | CSS opacity property | Applies to the entire element, not just the color — requires layout tree traversal | | Nested opacity | Parent opacity: 0.5 affects child elements — requires CSS inheritance chain |

Current Limitations

  • Background inheritance (same file) — Child elements inherit background-color from ancestor elements within the same .astro file (inline styles, Tailwind classes, and CSS rules). Cross-file inheritance (e.g. layout → page) requires --css
  • Pseudo-class colors included — CSS rules with :hover, :focus, :active are analyzed against the base element (the pseudo-class is stripped). This means hover/focus colors are checked for contrast, but are not distinguished from base-state colors in the output
  • No media queries — Selectors inside @media are not scope-aware. Attribute selectors ([data-x]) are stripped during matching
  • No sibling combinators — Adjacent sibling (+) and general sibling (~) selectors are matched by the target part only (the context is ignored)
  • No dynamic font sizesclamp(), min(), max(), calc(), and viewport units (vw, vh) cannot be resolved; text is treated as normal size (stricter threshold)
  • SCSS/SASS nesting supported<style lang="scss"> blocks are parsed: nested selectors (.card { .title { } }.card .title), & parent references (.btn { &:hover { } }.btn:hover), and plain CSS declarations are all extracted. SCSS-only features ($variables, @mixin, @include, @extend) are ignored — those values won't be resolved. If you need preprocessor variables, compile them to CSS custom properties and use --css or --tokens
  • No dark mode / theme scopes — Only :root and html custom properties are extracted. Variables under [data-theme="dark"], .dark, or @media (prefers-color-scheme: dark) are not resolved. Workaround: use separate CSS files per theme and run with --css themes/light.css or --css themes/dark.css

License

MIT