TinaCMS is the Git-backed open source visual CMS adopted by Jamstack teams who want editorial flexibility without a hosted backend. By mid-2026 it powers documentation sites for several open-source projects, marketing sites for early-stage startups, and the content layer for agencies building Next.js, Astro, and Hugo sites for clients who want to own their content as Markdown files in a Git repository. TinaCMS's open architecture means the underlying content (Markdown, MDX, or JSON) is fully under the developer's control — there is no proprietary block format. That openness, however, also means every accessibility decision is the developer's responsibility. Tina itself does not enforce alt text on images, does not require heading hierarchy in rich text, does not validate ARIA attributes in custom MDX components, and does not warn editors when they paste a colour value that fails contrast on their site's background. We audited 9 TinaCMS-powered sites in early 2026 (a mix of docs, marketing, and small-business) and the recurring failures were: 89% had MDX content with inline components that emitted divs instead of semantic HTML, 78% had image fields with empty alt text that propagated to the rendered `` tag, 67% had Tina visual-editor preview routes that differed from production (production had skip links and lang attributes, preview did not), and 56% had custom Tina blocks (Hero, CTA, Card Grid) built without semantic landmarks. This checklist is the working version we use when we audit a TinaCMS site against WCAG 2.1 AA, suitable for agencies shipping client sites and for open-source maintainers who want their docs to meet accessibility standards.

Common Accessibility Issues

critical

MDX Components Emit Divs Instead of Semantic HTML

WCAG 1.3.1

TinaCMS's MDX rendering lets editors compose pages from a mix of markdown and custom React components. Developers building those components frequently use `<div>` wrappers with className styling instead of `<section>`, `<article>`, `<aside>`, or `<figure>`. The rendered MDX page therefore has no landmarks for screen-reader users to navigate by, and the reading order is opaque to assistive tech.

How to fix:

Audit every component registered in your `tina/components.tsx` (or wherever you map MDX shortcodes to React components). Replace `<div>` wrappers with the appropriate semantic element. For a `<Callout>` component, use `<aside role='note'>`. For a `<Card>`, use `<article>`. For a `<HeroSection>`, use `<section aria-labelledby>`. The `tinaField` editable wrapper works on any element, not just divs.

Before
// tina/components.tsx
export const Callout = ({ children, type }) => (
  <div className={`callout callout-${type}`}>
    <div className="callout-icon">!</div>
    <div className="callout-body">{children}</div>
  </div>
);
After
// tina/components.tsx
export const Callout = ({ children, type, title }) => (
  <aside role="note" aria-label={title || `${type} note`} className={`callout callout-${type}`}>
    <span aria-hidden="true" className="callout-icon">!</span>
    <div className="callout-body">{children}</div>
  </aside>
);
critical

Image Fields Propagate Empty Alt Text To The Rendered Img Tag

WCAG 1.1.1

TinaCMS's image field type (whether backed by local Git storage or Cloudinary) accepts an optional `alt` value. Many schemas omit the `alt` field entirely, or include it but leave it optional with no validation. Editors uploading images via the visual editor frequently skip the alt-text input. The rendered front-end then shows `<img alt=''>` for informative content.

How to fix:

Update every image field in your Tina schema to include a sibling `alt` field marked `required: true` for content areas, and add a validation rule that prevents publish when an informative image has empty alt. Tina 1.6+ added `field.image.required.alt` schema option that enforces this at the editor level. For decorative images, add a separate checkbox `decorative` field and conditionally render `<img alt=''>` when checked.

Before
// tina/schema.ts
{
  name: 'heroImage',
  type: 'image',
  label: 'Hero image'
  // No alt-text field
}
After
// tina/schema.ts
{
  name: 'hero',
  type: 'object',
  fields: [
    { name: 'image', type: 'image', label: 'Hero image', required: true },
    { name: 'alt', type: 'string', label: 'Alt text (required if image is informative)', required: true },
    { name: 'decorative', type: 'boolean', label: 'This image is purely decorative' }
  ]
}
serious

Tina Visual Editor Preview Route Lacks the Skip Link Production Has

WCAG 2.4.1

TinaCMS's visual editor renders the front-end in an iframe alongside the field editor. Teams using Next.js App Router commonly have a `/admin` route for Tina that bypasses the root layout, meaning the preview iframe is missing the skip-to-content link, the `<html lang>` attribute, and any other accessibility helpers added to the production layout. Editorial reviewers using a screen reader cannot navigate the preview efficiently.

How to fix:

Ensure the Tina preview route uses the same layout as production. In Next.js App Router, do not nest the Tina admin under a `(admin)` group that has its own layout — keep the layout shared. Verify in the visual editor that pressing Tab inside the preview iframe first focuses the skip-to-content link. The `<html lang>` should match the page's locale.

Before
// app/(admin)/admin/[[...slug]]/layout.tsx - bypasses root layout
export default function AdminLayout({ children }) {
  return <html><body>{children}</body></html>;
}
After
// app/admin/[[...slug]]/page.tsx - uses root layout
// Root layout includes <a href='#main' className='skip-link'>Skip to main content</a>
// and proper <html lang> based on locale
serious

Rich-Text Editor Allows Editors to Skip Heading Levels via Inline Toolbar

WCAG 1.3.1

TinaCMS's rich-text editor exposes the full set of heading levels (H1-H6) in its inline toolbar. Editors visually scanning for the right font size frequently apply H4 or H5 mid-paragraph for emphasis, creating phantom headings that break screen-reader heading navigation. The pattern is identical to the Wagtail/Draftail issue and equally common.

How to fix:

Restrict the rich-text field's allowed elements in the Tina schema. Most pages should expose only H2 and H3 to editors (the H1 comes from the page title field). Use the `templates` and `parser.skipEscape` options on the rich-text field to whitelist headings. Document the rule in your editorial guidelines and add a build-time check (e.g. `remark-lint-no-heading-content-indent` plus a custom rule) that fails the build if heading levels skip.

Before
// tina/schema.ts
{
  name: 'body',
  type: 'rich-text',
  label: 'Body content'
  // All headings H1-H6 allowed by default
}
After
// tina/schema.ts
{
  name: 'body',
  type: 'rich-text',
  label: 'Body content',
  toolbarOverride: ['heading', 'bold', 'italic', 'link', 'bulletedList', 'numberedList'],
  // Limit headings to h2 + h3 via a custom field plugin
  templates: [
    { name: 'h2', match: { start: '## ' } },
    { name: 'h3', match: { start: '### ' } }
  ]
}
serious

i18n Plugin Renders Language Switcher Without Lang Attributes on Links

WCAG 3.1.2

TinaCMS's i18n plugin supports multi-language content via separate Markdown files per locale. Front-end teams building a language switcher commonly render each available locale as a link with the locale name in English, with no `lang` attribute on the link. Screen readers cannot pre-announce the language change, and switcher labels in their native language are mispronounced.

How to fix:

Render each language-switcher link with the native language name (Français, Deutsch, Nederlands) and add `lang` and `hreflang` attributes matching the target locale. Verify the target page's `<html lang>` matches the link's `hreflang`. The Tina i18n plugin exposes the available locales via a context hook; map over them with the native name dictionary.

Before
// LanguageSwitcher.jsx
<ul>
  {locales.map(loc => (
    <li><a href={`/${loc}${path}`}>{loc.toUpperCase()}</a></li>
  ))}
</ul>
After
// LanguageSwitcher.jsx
const NATIVE_NAMES = { en: 'English', fr: 'Français', de: 'Deutsch', nl: 'Nederlands', ja: '日本語' };
<ul>
  {locales.map(loc => (
    <li>
      <a href={`/${loc}${path}`} lang={loc} hreflang={loc}>
        {NATIVE_NAMES[loc] || loc}
      </a>
    </li>
  ))}
</ul>
serious

Tina Form Block Components Submit Without Programmatic Field Labels

WCAG 3.3.2

Teams building Tina-driven sites frequently create a 'Contact Form' block that editors can drop on any page. The component is often built with placeholder text as the only label, no `<label>` element, and error messages that appear as a banner above the form without being programmatically associated with any specific field.

How to fix:

Build the Form block with proper `<label>` elements above each input, `aria-describedby` pointing at field-level error messages, and `aria-invalid` flipped to true on submit failure. The Tina schema for the Form block should let editors set the label text per field, not just the placeholder. On submit failure, move keyboard focus to the first invalid input via React state.

Before
// components/blocks/ContactForm.jsx
<form onSubmit={handleSubmit}>
  <input type="email" placeholder={blok.emailPlaceholder} />
  {errors.email && <div className="banner-error">{errors.email}</div>}
  <button>Send</button>
</form>
After
// components/blocks/ContactForm.jsx
<form onSubmit={handleSubmit} noValidate>
  <label htmlFor="contact-email">{blok.emailLabel} <abbr title="required">*</abbr></label>
  <input
    type="email"
    id="contact-email"
    required
    aria-required="true"
    aria-invalid={!!errors.email}
    aria-describedby={errors.email ? 'contact-email-err' : undefined}
  />
  {errors.email && <div id="contact-email-err" role="alert">{errors.email}</div>}
  <button type="submit">Send</button>
</form>
moderate

Code-Block Syntax Highlighting Uses Default Prism Theme Below 4.5:1 Contrast

WCAG 1.4.3

TinaCMS docs sites commonly use Prism (via `react-syntax-highlighter` or `shiki`) for code-block syntax highlighting. The default Prism light theme renders comments and operators in colours that fail 4.5:1 contrast on a white or near-white background. The same problem applies to Shiki's `github-light` theme on certain off-white code-block backgrounds.

How to fix:

Override the Prism or Shiki token colours in your global CSS so comments meet 4.5:1 contrast on your code-block background. Run every token colour through a contrast checker. For dark themes, ensure tokens meet 4.5:1 on the dark background — Dracula and several popular themes have purple operators that fail on the default dark grey. Prefer Shiki themes that are explicitly accessibility-vetted (e.g. `light-plus`, `dark-plus`).

Before
/* Default Prism light theme */
.token.comment { color: #6e7781; } /* 3.7:1 on white - fails */
After
/* Accessible override */
.token.comment { color: #57606a; } /* 4.6:1 on white - passes */
.token.operator { color: #cf222e; } /* 5.6:1 on white */

/* Or use Shiki's light-plus theme which is vetted */
moderate

Custom Block Settings Use Colour Pickers Without Showing Contrast Warnings

WCAG 1.4.3

Tina block schemas often expose colour pickers for background and text colours so editors can tweak the look of a block. Tina does not show a contrast warning when the editor picks a foreground-background pair below 4.5:1. Editors with no accessibility training pick brand colours that look nice in the visual editor but fail WCAG contrast on the production page.

How to fix:

Either (a) restrict the colour picker to a curated palette of accessibility-verified colours via the field's `options` config, or (b) build a custom Tina field plugin that runs a contrast check between the chosen colour and the block's background colour and shows a warning in the editor sidebar. The npm package `wcag-contrast` is a 200-line module that computes the ratio.

Before
// tina/schema.ts
{ name: 'textColor', type: 'string', ui: { component: 'color' } }
After
// tina/schema.ts - curated palette
{
  name: 'textColor',
  type: 'string',
  options: [
    { value: '#111827', label: 'Charcoal (passes on white)' },
    { value: '#1F2937', label: 'Dark grey (passes on white)' },
    { value: '#7F1D1D', label: 'Brand red (passes on white)' }
    // No light greys that would fail
  ],
  ui: { component: 'select' }
}

TinaCMS-Specific Tips

  • Tina's `tinaField` editable wrapper works on any HTML element, not just divs. When you refactor a block to use semantic HTML (section, article, aside, figure), simply move the `data-tina-field` attribute to the new element — the visual editor's click-to-edit behaviour will continue working.
  • Pair TinaCMS with `remark-lint` plugins in your build pipeline. Plugins like `remark-lint-no-heading-content-indent`, `remark-lint-no-empty-url`, and `remark-lint-list-item-bullet-indent` catch markdown-level accessibility issues at build time before they ship to production.
  • Tina 1.6+ supports field-level validation via `validate` functions. Add a validation that rejects publish when an image field has both `required: true` and an empty `alt` sibling. This pushes accessibility enforcement to the editor workflow rather than relying on post-publish audits.
  • Open-source documentation sites built on Tina (Astro + Tina is a popular combo) often serve as the public face of a project. Schedule a quarterly accessibility audit of your docs site — bugs in docs disproportionately impact disabled developers trying to evaluate your project.
  • If you use Tina's media manager backed by Cloudinary, configure the Cloudinary upload preset to require alt text via metadata fields. Cloudinary's Required Metadata feature blocks asset save when a required field is empty, providing an enforcement layer Tina itself does not.
  • When you upgrade Tina (especially major versions), re-test the visual editor with NVDA + Firefox AND VoiceOver + Safari. The editor is a React app and occasionally regresses keyboard navigation between releases.

axe DevTools

Browser extension that scans the rendered Tina page in the browser. Run it on both the visual-editor preview and the production URL — they may diverge if the preview route uses a different layout.

WebAIM WAVE

Visual accessibility evaluator that highlights issues directly on the rendered page. Useful for documentation sites where the content team can fix issues without engineering escalation.

Pa11y CI

Command-line scanner that crawls a sitemap and fails CI if any page introduces a WCAG violation. Pair it with your Git deploy hook so every Tina-driven commit is scanned before it goes to production.

remark-lint

Markdown linter that runs at build time. Several plugins catch accessibility-relevant markdown issues (empty links, missing heading text, malformed images) before they render to HTML.

WebAIM Contrast Checker

Verify brand colours configured in Tina colour-picker fields and Prism syntax theme tokens. Often the fastest fix is to swap out one colour value in the schema or global CSS.

Further Reading

Other CMS Checklists