Category: Front-End Development

  • Accessibility jobs dashboard

    The Accessibility jobs dashboard came about due to personal curiosity about building a web scraper using Python to collect and filter accessibility job listings from a11yjobs.com, then send notifications. I found the site to be a great resource for accessibility roles, but until their recent site updates, I couldn’t get emails about the new posted jobs. As an accessibility consultant, working with a team 6+ hours away can disqualify me from a role before I even apply. Generic job alert emails don’t account for that.

    When I built the scraper, I implemented filtered notifications: a daily email with accessibility jobs posted in North America that are remote, and Seattle-area roles that are hybrid or on-site. All the new jobs are scraped and saved into a database; the dashboard fetches the API data and displays it on the page with sorting, filtering and search of the accessibility jobs.

    The dashboard needed to be accessible too. Building it that way, component by component, is what led to the next piece: I’m extracting an accessible React component library from the dashboard so others can use the same pieces (A11yTable is the first one, in progress now).

    Building A11yTable

    The dashboard’s table needed to handle sorting, filtering, and search on job listings, and it needed to be accessible by default, not accessible as an afterthought. Most table libraries treat accessibility as something you bolt on with extra props. I wanted the opposite: an accessible table component that could sit on top of different table engines, so the accessibility layer wasn’t tied to one library’s decisions.

    That led to a TableEngine interface with two implementations: one backed by TanStack Table, and one a zero-dependency naive engine I wrote from scratch. Both engines expose the same interface, so A11yTable doesn’t care which one powers it underneath. This also meant I could write a dual-engine equivalence test, running the same test suite against both engines and confirming they produce identical output.

    A few decisions I settled on for the API:

    • `header` is a plain string, not a `ReactNode`. This keeps the accessible name predictable instead of leaving it to whatever markup a consumer passes in.
    • `aria-sort` is only ever applied to the active sorted column, never to all columns with a “none” value. This matches how screen readers announce sort state, rather than technically satisfying the spec while adding noise.
    • Sorting is toggle-only. No tri-state (ascending, descending, unsorted) cycling. Fewer states, fewer chances for a screen reader user to lose track of what’s happening.
    • Filtering is out of scope for the component itself. That’s a deliberate boundary, not a missing feature.
    • An `announce={false}` prop lets a consuming app own its own live-region announcements, for cases where the app already has an announcement pattern and doesn’t want A11yTable fighting with it.

    Testing is Vitest and Testing Library for behavior, vitest-axe for automated accessibility checks, plus the dual-engine equivalence test. 22 tests are passed right now. Two gotchas worth naming for anyone doing similar work: Vitest requires `globals: true` in its config for Testing Library’s auto-cleanup to run between tests, and Vitest 4 changed custom matcher type augmentation to a unified `Matchers` interface, which broke a few examples I found while setting this up.

    Making the chart accessible

    The table wasn’t the only piece that needed work. The dashboard also has a Recharts pie chart showing the top job titles by posting count, and Recharts renders a pie chart as a collection of individual SVG shapes and text nodes, each one a slice, a label, a path. None of that has any inherent meaning to a screen reader. Depending on how it’s exposed, a screen reader user either gets nothing, or gets a stream of disconnected fragments as they navigate into the SVG.

    The fix was to wrap the whole chart in a <figure> with a single aria-label that’s built from the actual data: something like “Top 5 job titles by posting count: Accessibility Specialist (12 jobs), Front End Developer (8 jobs), …” generated by mapping over the same data the chart renders from. Adding tabIndex={0} to the figure makes it focusable, so a keyboard or screen reader user can tab to the chart and get one complete, accurate description in a single stop, instead of the SVG internals being announced piecemeal or skipped entirely.

    Because the aria-label already carries the full description, there’s no separate visually-hidden text needed alongside it, one label, one source of truth, no risk of the visible chart and the accessible description drifting out of sync as the data changes.

    It’s a small pattern, but an easy one to skip, since a chart can look complete visually while giving a screen reader user nothing meaningful to work with.

    Where this is headed

    Both of these, the table and the chart, came from the same underlying rule: accessibility isn’t something to check for after the component works, it’s part of what “working” means. That’s slower to build. It’s also the only way I know of to end up with something I’d actually want to ship to a client.

    A11yTable is the first component coming out of the dashboard; a few more are on the way as I keep extracting pieces from it.

  • Let’s start with something simple – CSS variables

    Custom properties, sometimes referred to as CSS variables or cascading variables enable developers to reuse defined values in properties throughout the web application style sheet. The variables are defined in the :root style sheet and automatically inherited by the style sheets in the app. The variables are prepended with ‘–‘ like ‘–text-color’ and referenced like this in the style sheet: color: var(–text-color);

    Image of CSS root definition

    Most web sites, and even the most creatively designed websites, have a small palette of colors throughout the site. Making them predefined in what is almost like an index for the CSS makes it simple to reference and implement. Developers new to the code base and project do not need to go far to discover the colors needed to use.

    This approach simplifies updates during redesigns or when addressing color contrast issues for accessibility. By modifying the central “index,” developers can propagate changes across large websites effortlessly, reducing errors in managing colors across hundreds or thousands of lines of code.

    Image of CSS selector using CSS variables

    These variables are not limited to colors, but this is a great use case for custom properties. Font sizes and animations come to mind as other useful implementations.

    Example of responsive design with CSS variables:

    @media (min-width: 768px) { :root { –font-size: 18px; }}

    Define animation timing or easing functions for consistent, reusable animations:

    –animation-duration: 300ms; transition: all var(–animation-duration) ease-in-out;

    Include a fallback color for older browsers:

    color: var(–text-color, black);

    Custom properties worked well in the implimentation of a light mode/dark mode toggle button I added to the family tree website with React context. The button basically switched the chosen :root css for light or dark and changed the colors across all the components in the application.

    This is a fairly simple but useful demonstration of custom properties in CSS. If you want to learn more about CSS variables or custom properties visit: MDN Web Docs

  • Is this thing on?

    My first attempt at blogging was around the early 2010s where I began blogging about my cycling activity to help promote a Livestrong century challenge I was participating in and raising money for. The blog evolved in to my participation in local cyclocross races and long distance events and eventually covering hiking adventures and travel. The blog is still online after all these years and you could still be one of the first 20 people to ever see it at hawgfuel on blogspot.

    My blog posts here will be about topics in web development I find interesting to share, and will typically be about things I’m working on. The interest in the blog may not be much more than my outdoors blog in the past, but it will be a good place for me to consolidate some thoughts that may help lead to something bigger. Of course this will also allow me to experiment with Word Press to add to my experience with CMS software and provide examples on Guicoder as I have with a headless CMS for the family tree website.