Menu

Tailwind CSS Screen Readers

Screen readers are softwares that make web content accessible to individuals with visual impairments. Screen reader utilities are typically used to hide content visually but keep it accessible for assistive technologies like screen readers.

In this help document, we'll learn how to work with screen-reader utilities in Tailwind CSS, including how to make elements screen reader-only, undo these properties, conditionally apply them based on states, leverage responsive design, and more.

ClassPropertiesExample
sr-onlyposition: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border-width: 0;<div className="sr-only"></div>
not-sr-onlyposition: static; width: auto; height: auto; padding: 0; margin: 0; overflow: visible; clip: auto; white-space: normal;<div className="not-sr-only"></div>

Overview of Screen Readers

Adding the sr-only

The sr-only utility in Tailwind CSS is used to ensure an element is hidden from the visual interface but remains available for assistive technologies.

In this example, the image is effectively hidden from sight but will remain accessible to screen readers. Remove the sr-only class in the above code to make the image visibile.

This is a live editor. Play around with it!
export default function ScreenReaderOnlyImage() {
  return (
    <div className="w-screen h-screen bg-gray-100 flex flex-col gap-8 items-center justify-center">
      <img
        src="https://images.unsplash.com/photo-1489269637500-aa0e75768394"
        alt=""
        className="sr-only h-24 w-32"
      />
      <p className="px-4 py-2">
        There's an image above this text.
      </p>
    </div>
  );
}

Adding the not-sr-only

Occasionally, you might need to undo the sr-only(e.g., on bigger screens) behavior applied to a specific element. Tailwind provides a not-sr-only utility that reverses these properties, making the element visible again.

This is a live editor. Play around with it!
export default function RevertScreenReaderText() {
  return (
    <div className="w-screen h-screen bg-gray-100 flex items-center justify-center">
      <span className="sr-only">Hidden for visual users</span>
      <span className="not-sr-only sr-only p-4 bg-green-200">
        Visible after applying 'not-sr-only'
      </span>
    </div>
  );
}

States and Responsiveness

Hover and Focus States

Tailwind also allows you to add screen-reader utilities to states like hover or focus, ensuring accessibility inside interactive elements.

This is a live editor. Play around with it!
export default function InteractiveState() {
  return (
    <div className="w-screen h-screen bg-gray-100 flex items-center justify-center">
        <p tabindex="0" className="focus:sr-only">
          Only for screen readers on focus
        </p>
    </div>
  );
}

Breakpoint Modifiers

Responsive designs often require specific accessibility adaptations across screen sizes. Tailwind offers utilities like md:sr-only and lg:not-sr-only to adjust behaviors at different breakpoints.

This is a live editor. Play around with it!
export default function ResponsiveText() {
  return (
    <div className="w-screen h-screen bg-gray-100 flex items-center justify-center px-10">
      <p className="text-center md:sr-only lg:not-sr-only">
        This text is hidden for medium devices and visible for large devices.
      </p>
    </div>
  );
}

Real World Examples

Product Card Grid

A responsive product grid displaying electronics with hidden details for screen readers.

This is a live editor. Play around with it!
const ProductGrid = () => {
  const products = [
    {
      id: 1,
      name: "Ultra HD Smart TV",
      price: 899.99,
      stock: 15,
      rating: 4.5,
      src: "https://images.unsplash.com/photo-1593359677879-a4bb92f829d1",
      alt: "55-inch Smart TV displaying vibrant nature scene",
      label: "Television product details",
      specs: "4K resolution, HDR support, Smart TV features"
    },
    {
      id: 2,
      name: "Professional Camera",
      price: 1299.99,
      stock: 8,
      rating: 4.8,
      src: "https://images.unsplash.com/photo-1516035069371-29a1b244cc32",
      alt: "Professional DSLR camera with attached lens",
      label: "Camera product details",
      specs: "24.2MP sensor, 4K video, Weather-sealed body"
    },
    {
      id: 3,
      name: "Wireless Headphones",
      price: 249.99,
      stock: 23,
      rating: 4.6,
      src: "https://images.unsplash.com/photo-1505740420928-5e560c06d30e",
      alt: "Over-ear wireless headphones in matte black",
      label: "Headphones product details",
      specs: "Active noise cancellation, 30-hour battery"
    },
    {
      id: 4,
      name: "Gaming Console",
      price: 499.99,
      stock: 12,
      rating: 4.9,
      src: "https://images.unsplash.com/photo-1486401899868-0e435ed85128",
      alt: "Next-gen gaming console with controller",
      label: "Gaming console product details",
      specs: "8K support, Ray tracing, 1TB SSD"
    },
    {
      id: 5,
      name: "Smartphone",
      price: 799.99,
      stock: 19,
      rating: 4.7,
      src: "https://images.unsplash.com/photo-1511707171634-5f897ff02aa9",
      alt: "Latest model smartphone showing home screen",
      label: "Smartphone product details",
      specs: "6.7-inch OLED, Triple camera system"
    },
    {
      id: 6,
      name: "Tablet",
      price: 649.99,
      stock: 16,
      rating: 4.4,
      src: "https://images.unsplash.com/photo-1585790050230-5dd28404ccb9",
      alt: "Slim tablet device with stylus",
      label: "Tablet product details",
      specs: "10.9-inch Liquid Retina, M1 chip"
    }
  ];

  return (
    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 p-6">
      {products.map((product) => (
        <div key={product.id} className="border rounded-lg p-4 hover:shadow-lg transition-shadow">
          <div className="relative">
            <img 
              src={product.src} 
              alt={product.alt}
              className="w-full h-48 object-cover rounded-md"
            />
            <span className="sr-only">{product.label}</span>
          </div>
          
          <h3 className="text-xl font-semibold mt-4">{product.name}</h3>
          
          <div className="mt-2">
            <span className="text-lg font-bold">${product.price}</span>
            <span className="sr-only">Price in dollars</span>
          </div>
          
          <div className="mt-2 flex items-center">
            <span className="sr-only">{`Rating: ${product.rating} out of 5 stars`}</span>
            <div className="flex text-yellow-400">
              {[...Array(5)].map((_, i) => (
                <svg
                  key={i}
                  className={`w-5 h-5 ${i < Math.floor(product.rating) ? 'text-yellow-400' : 'text-gray-300'}`}
                  fill="currentColor"
                  viewBox="0 0 20 20"
                >
                  <path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
                </svg>
              ))}
            </div>
          </div>

          <div className="mt-2">
            <span className="not-sr-only text-sm text-gray-600">In Stock: {product.stock}</span>
            <span className="sr-only">{`${product.stock} units available in stock`}</span>
          </div>

          <p className="sr-only">{product.specs}</p>
        </div>
      ))}
    </div>
  );
};

export default ProductGrid;

Email Notification Panel

A notification panel showing email updates with proper screen reader support.

This is a live editor. Play around with it!
const EmailNotifications = () => {
  const notifications = [
    {
      id: 1,
      type: "promotion",
      subject: "Special Holiday Offer!",
      sender: "Fashion Store",
      email: "promotions@fashionstore.com",
      preview: "Get 50% off on all winter collection items...",
      time: "10:30 AM",
      isUnread: true,
      priority: "high",
      category: "Promotions"
    },
    {
      id: 2,
      type: "invoice",
      subject: "Your Invoice #2458 is Ready",
      sender: "Billing Department",
      email: "billing@company.com",
      preview: "Please find attached your monthly invoice...",
      time: "9:45 AM",
      isUnread: true,
      priority: "medium",
      category: "Bills"
    },
    {
      id: 3,
      type: "meeting",
      subject: "Team Meeting Reminder",
      sender: "Project Manager",
      email: "pm@company.com",
      preview: "Don't forget our weekly team sync at 2 PM...",
      time: "9:00 AM",
      isUnread: false,
      priority: "high",
      category: "Work"
    },
    {
      id: 4,
      type: "social",
      subject: "New Connection Request",
      sender: "Professional Network",
      email: "notifications@network.com",
      preview: "John Doe wants to connect with you...",
      time: "8:15 AM",
      isUnread: true,
      priority: "low",
      category: "Social"
    },
    {
      id: 5,
      type: "security",
      subject: "Security Alert: New Login",
      sender: "Security Team",
      email: "security@company.com",
      preview: "We detected a new login from an unknown device...",
      time: "7:30 AM",
      isUnread: true,
      priority: "critical",
      category: "Security"
    },
    {
      id: 6,
      type: "newsletter",
      subject: "Your Weekly Tech Digest",
      sender: "Tech News Daily",
      email: "news@techdaily.com",
      preview: "Top stories in technology this week...",
      time: "7:00 AM",
      isUnread: false,
      priority: "low",
      category: "News"
    }
  ];

  return (
    <div className="max-w-2xl mx-auto p-4">
      <div className="bg-white rounded-lg shadow">
        <div className="border-b px-4 py-3">
          <h2 className="text-lg font-semibold">Email Notifications</h2>
          <span className="sr-only">You have {notifications.filter(n => n.isUnread).length} unread messages</span>
        </div>

        <ul className="divide-y">
          {notifications.map((notification) => (
            <li key={notification.id} className={`p-4 hover:bg-gray-50 ${notification.isUnread ? 'bg-blue-50' : ''}`}>
              <div className="flex items-center justify-between">
                <div className="flex-1 min-w-0">
                  <div className="flex items-center">
                    <span 
                      className={`w-2 h-2 rounded-full mr-2 ${
                        notification.priority === 'critical' ? 'bg-red-500' :
                        notification.priority === 'high' ? 'bg-orange-500' :
                        notification.priority === 'medium' ? 'bg-yellow-500' :
                        'bg-green-500'
                      }`}
                      aria-hidden="true"
                    />
                    <span className="sr-only">Priority: {notification.priority}</span>
                    
                    <p className="text-sm font-medium text-gray-900 truncate">
                      {notification.subject}
                    </p>
                  </div>

                  <div className="mt-1">
                    <p className="text-sm text-gray-500 truncate">
                      <span className="not-sr-only">From: {notification.sender}</span>
                      <span className="sr-only">Email from {notification.sender} at {notification.email}</span>
                    </p>
                  </div>

                  <p className="mt-1 text-sm text-gray-600">{notification.preview}</p>
                </div>

                <div className="ml-4 flex-shrink-0">
                  <span className="text-sm text-gray-500">{notification.time}</span>
                  {notification.isUnread && (
                    <span className="sr-only">This message is unread</span>
                  )}
                  <div className="mt-1 text-xs text-gray-500">
                    <span className="not-sr-only">{notification.category}</span>
                    <span className="sr-only">Category: {notification.category}</span>
                  </div>
                </div>
              </div>
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
};

export default EmailNotifications;

Task Management Dashboard

A task management interface with screen reader optimizations for task status and priorities.

This is a live editor. Play around with it!
const TaskDashboard = () => {
  const tasks = [
    {
      id: 1,
      title: "Update Website Content",
      description: "Review and update the main landing page content",
      assignee: "Sarah Parker",
      avatar: "https://images.unsplash.com/photo-1494790108377-be9c29b29330",
      dueDate: "2024-02-10",
      priority: "high",
      status: "in-progress",
      progress: 75,
      tags: ["website", "content", "marketing"]
    },
    {
      id: 2,
      title: "Fix Payment Integration",
      description: "Debug and resolve payment gateway issues",
      assignee: "Michael Chen",
      avatar: "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e",
      dueDate: "2024-02-12",
      priority: "critical",
      status: "pending",
      progress: 30,
      tags: ["backend", "payments", "bug"]
    },
    {
      id: 3,
      title: "Design New Logo",
      description: "Create new brand logo variations",
      assignee: "Emma Wilson",
      avatar: "https://images.unsplash.com/photo-1438761681033-6461ffad8d80",
      dueDate: "2024-02-15",
      priority: "medium",
      status: "review",
      progress: 90,
      tags: ["design", "branding"]
    },
    {
      id: 4,
      title: "Mobile App Testing",
      description: "Conduct user testing for new mobile features",
      assignee: "James Brown",
      avatar: "https://images.unsplash.com/photo-1500648767791-00dcc994a43e",
      dueDate: "2024-02-18",
      priority: "high",
      status: "pending",
      progress: 0,
      tags: ["mobile", "testing", "UX"]
    },
    {
      id: 5,
      title: "Write Documentation",
      description: "Create technical documentation for API",
      assignee: "Lisa Zhang",
      avatar: "https://images.unsplash.com/photo-1544005313-94ddf0286df2",
      dueDate: "2024-02-20",
      priority: "low",
      status: "completed",
      progress: 100,
      tags: ["documentation", "API"]
    },
    {
      id: 6,
      title: "Server Maintenance",
      description: "Perform routine server maintenance and updates",
      assignee: "Alex Johnson",
      avatar: "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e",
      dueDate: "2024-02-22",
      priority: "medium",
      status: "in-progress",
      progress: 45,
      tags: ["DevOps", "maintenance"]
    }
  ];

  return (
    <div className="max-w-4xl mx-auto p-6">
      <div className="bg-white rounded-lg shadow">
        <div className="px-6 py-4 border-b">
          <h2 className="text-xl font-semibold">Task Dashboard</h2>
          <span className="sr-only">Current task overview and status</span>
        </div>

        <div className="divide-y">
          {tasks.map((task) => (
            <div key={task.id} className="p-6 hover:bg-gray-50">
              <div className="flex items-center justify-between">
                <div className="flex-1">
                  <div className="flex items-center">
                    <h3 className="text-lg font-medium">{task.title}</h3>
                    <span 
                      className={`ml-3 px-2 py-1 text-xs rounded-full ${
                        task.priority === 'critical' ? 'bg-red-100 text-red-800' :
                        task.priority === 'high' ? 'bg-orange-100 text-orange-800' :
                        task.priority === 'medium' ? 'bg-yellow-100 text-yellow-800' :
                        'bg-green-100 text-green-800'
                      }`}
                    >
                      <span className="not-sr-only capitalize">{task.priority}</span>
                      <span className="sr-only">Task priority: {task.priority}</span>
                    </span>
                  </div>

                  <p className="mt-2 text-sm text-gray-600">{task.description}</p>

                  <div className="mt-4">
                    <div className="flex items-center">
                      <img
                        src={task.avatar}
                        alt=""
                        className="w-8 h-8 rounded-full"
                        aria-hidden="true"
                      />
                      <span className="ml-2 text-sm text-gray-600">
                        <span className="not-sr-only">{task.assignee}</span>
                        <span className="sr-only">Assigned to {task.assignee}</span>
                      </span>
                    </div>
                  </div>

                  <div className="mt-4">
                    <div className="flex items-center">
                      <div className="w-full bg-gray-200 rounded-full h-2">
                        <div
                          className={`h-2 rounded-full ${
                            task.status === 'completed' ? 'bg-green-500' :
                            task.status === 'in-progress' ? 'bg-blue-500' :
                            task.status === 'review' ? 'bg-purple-500' :
                            'bg-gray-500'
                          }`}
                          style={{ width: `${task.progress}%` }}
                          aria-hidden="true"
                        />
                      </div>
                      <span className="ml-2 text-sm text-gray-600">
                        <span className="not-sr-only">{task.progress}%</span>
                        <span className="sr-only">Task is {task.progress} percent complete</span>
                      </span>
                    </div>
                  </div>

                  <div className="mt-4 flex flex-wrap gap-2">
                    {task.tags.map((tag) => (
                      <span
                        key={tag}
                        className="px-2 py-1 text-xs bg-gray-100 rounded-full"
                      >
                        <span className="not-sr-only">#{tag}</span>
                        <span className="sr-only">Tagged as {tag}</span>
                      </span>
                    ))}
                  </div>
                </div>

                <div className="ml-4 flex-shrink-0 text-sm text-gray-500">
                  <span className="not-sr-only">Due {task.dueDate}</span>
                  <span className="sr-only">Due date is {task.dueDate}</span>
                </div>
              </div>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
};

export default TaskDashboard;

Calendar Event List

A calendar event listing with enhanced screen reader support for event details and status.

This is a live editor. Play around with it!
const CalendarEvents = () => {
  const events = [
    {
      id: 1,
      title: "Annual Company Meeting",
      type: "company",
      date: "2024-02-15",
      startTime: "09:00",
      endTime: "11:00",
      location: "Main Conference Room",
      attendees: 45,
      description: "Annual company review and future planning",
      status: "upcoming",
      isRequired: true,
      organizer: {
        name: "Robert Johnson",
        avatar: "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e"
      }
    },
    {
      id: 2,
      title: "Product Launch Planning",
      type: "project",
      date: "2024-02-16",
      startTime: "14:00",
      endTime: "15:30",
      location: "Meeting Room B",
      attendees: 12,
      description: "Discuss Q1 product launch strategy",
      status: "draft",
      isRequired: true,
      organizer: {
        name: "Emma Davis",
        avatar: "https://images.unsplash.com/photo-1494790108377-be9c29b29330"
      }
    },
    {
      id: 3,
      title: "Team Training Session",
      type: "training",
      date: "2024-02-17",
      startTime: "10:00",
      endTime: "12:00",
      location: "Training Center",
      attendees: 25,
      description: "New software training for development team",
      status: "confirmed",
      isRequired: false,
      organizer: {
        name: "Michael Chen",
        avatar: "https://images.unsplash.com/photo-1500648767791-00dcc994a43e"
      }
    },
    {
      id: 4,
      title: "Client Presentation",
      type: "client",
      date: "2024-02-18",
      startTime: "13:00",
      endTime: "14:00",
      location: "Virtual Meeting",
      attendees: 8,
      description: "Present Q4 results to key client",
      status: "upcoming",
      isRequired: true,
      organizer: {
        name: "Sarah Wilson",
        avatar: "https://images.unsplash.com/photo-1438761681033-6461ffad8d80"
      }
    },
    {
      id: 5,
      title: "Budget Review",
      type: "finance",
      date: "2024-02-19",
      startTime: "11:00",
      endTime: "12:30",
      location: "Finance Department",
      attendees: 6,
      description: "Monthly budget review meeting",
      status: "draft",
      isRequired: true,
      organizer: {
        name: "David Lee",
        avatar: "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e"
      }
    },
    {
      id: 6,
      title: "Team Building Event",
      type: "social",
      date: "2024-02-20",
      startTime: "15:00",
      endTime: "17:00",
      location: "City Park",
      attendees: 30,
      description: "Outdoor team building activities",
      status: "confirmed",
      isRequired: false,
      organizer: {
        name: "Lisa Zhang",
        avatar: "https://images.unsplash.com/photo-1544005313-94ddf0286df2"
      }
    }
  ];

  return (
    <div className="max-w-4xl mx-auto p-6 bg-white rounded-lg shadow">
      <div className="border-b pb-4 mb-6">
        <h2 className="text-xl font-semibold">Upcoming Events</h2>
        <span className="sr-only">Calendar events for the next week</span>
      </div>

      <div className="space-y-6">
        {events.map((event) => (
          <div 
            key={event.id} 
            className={`p-4 rounded-lg border ${
              event.status === 'upcoming' ? 'border-blue-200 bg-blue-50' :
              event.status === 'confirmed' ? 'border-green-200 bg-green-50' :
              'border-gray-200 bg-gray-50'
            }`}
          >
            <div className="flex items-center justify-between">
              <div className="flex-1">
                <div className="flex items-center">
                  <h3 className="text-lg font-medium">{event.title}</h3>
                  {event.isRequired && (
                    <span className="ml-2 px-2 py-1 text-xs bg-red-100 text-red-800 rounded-full">
                      <span className="not-sr-only">Required</span>
                      <span className="sr-only">This event is mandatory</span>
                    </span>
                  )}
                </div>

                <div className="mt-2">
                  <span className="sr-only">
                    {`Event scheduled for ${event.date} from ${event.startTime} to ${event.endTime}`}
                  </span>
                  <p className="not-sr-only text-sm text-gray-600">
                    {event.date}{event.startTime} - {event.endTime}
                  </p>
                </div>

                <div className="mt-2 flex items-center">
                  <span className="not-sr-only text-sm text-gray-600">
                    📍 {event.location}
                  </span>
                  <span className="sr-only">Location: {event.location}</span>
                </div>

                <div className="mt-4">
                  <p className="text-sm text-gray-700">{event.description}</p>
                </div>

                <div className="mt-4 flex items-center">
                  <img
                    src={event.organizer.avatar}
                    alt=""
                    className="w-8 h-8 rounded-full"
                    aria-hidden="true"
                  />
                  <div className="ml-2">
                    <span className="not-sr-only text-sm text-gray-600">
                      Organized by {event.organizer.name}
                    </span>
                    <span className="sr-only">
                      Event organized by {event.organizer.name}
                    </span>
                  </div>
                </div>

                <div className="mt-2 flex items-center">
                  <span className="text-sm text-gray-600">
                    <span className="not-sr-only">
                      {event.attendees} attendees
                    </span>
                    <span className="sr-only">
                      {event.attendees} people attending this event
                    </span>
                  </span>
                  <span 
                    className={`ml-4 px-2 py-1 text-xs rounded-full ${
                      event.status === 'upcoming' ? 'bg-blue-100 text-blue-800' :
                      event.status === 'confirmed' ? 'bg-green-100 text-green-800' :
                      'bg-gray-100 text-gray-800'
                    }`}
                  >
                    <span className="not-sr-only capitalize">{event.status}</span>
                    <span className="sr-only">Event status: {event.status}</span>
                  </span>
                </div>
              </div>

              <div className="ml-4">
                <button 
                  className="text-blue-600 hover:text-blue-800"
                  aria-label={`View details for ${event.title}`}
                >
                  <svg 
                    className="w-6 h-6" 
                    fill="none" 
                    stroke="currentColor" 
                    viewBox="0 0 24 24"
                  >
                    <path 
                      strokeLinecap="round" 
                      strokeLinejoin="round" 
                      strokeWidth={2} 
                      d="M9 5l7 7-7 7" 
                    />
                  </svg>
                </button>
              </div>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
};

export default CalendarEvents;

Analytics Dashboard Card Grid

A dashboard displaying various analytics metrics with screen reader optimizations and interactive elements.

This is a live editor. Play around with it!
const AnalyticsDashboard = () => {
  const metrics = [
    {
      id: 1,
      title: "Total Revenue",
      value: "$128,459",
      change: "+12.5%",
      trend: "up",
      timeRange: "vs last month",
      icon: "https://images.unsplash.com/photo-1526304640581-d334cdbbf45e",
      details: {
        previousValue: "$114,230",
        bestDay: "March 15",
        worstDay: "March 3",
        averageDaily: "$4,282"
      },
      category: "financial"
    },
    {
      id: 2,
      title: "Active Users",
      value: "45,827",
      change: "-2.3%",
      trend: "down",
      timeRange: "vs last month",
      icon: "https://images.unsplash.com/photo-1519085360753-af0119f7cbe7",
      details: {
        previousValue: "46,905",
        peakTime: "2 PM EST",
        lowTime: "4 AM EST",
        averageSession: "18 minutes"
      },
      category: "users"
    },
    {
      id: 3,
      title: "Conversion Rate",
      value: "3.8%",
      change: "+0.5%",
      trend: "up",
      timeRange: "vs last month",
      icon: "https://images.unsplash.com/photo-1460925895917-afdab827c52f",
      details: {
        previousValue: "3.3%",
        highestRate: "4.2%",
        lowestRate: "2.9%",
        industryAvg: "3.1%"
      },
      category: "performance"
    },
    {
      id: 4,
      title: "Support Tickets",
      value: "284",
      change: "-15.2%",
      trend: "down",
      timeRange: "vs last month",
      icon: "https://images.unsplash.com/photo-1531538606174-0f90ff5dce83",
      details: {
        previousValue: "335",
        avgResponseTime: "45 minutes",
        resolvedTickets: "256",
        pendingTickets: "28"
      },
      category: "support"
    },
    {
      id: 5,
      title: "New Orders",
      value: "1,483",
      change: "+8.4%",
      trend: "up",
      timeRange: "vs last month",
      icon: "https://images.unsplash.com/photo-1486406146926-c627a92ad1ab",
      details: {
        previousValue: "1,368",
        avgOrderValue: "$86",
        topProduct: "Premium Plan",
        repeatCustomers: "42%"
      },
      category: "sales"
    },
    {
      id: 6,
      title: "Cart Abandonment",
      value: "22.4%",
      change: "-1.8%",
      trend: "down",
      timeRange: "vs last month",
      icon: "https://images.unsplash.com/photo-1553729459-efe14ef6055d",
      details: {
        previousValue: "24.2%",
        recoveredCarts: "45",
        avgCartValue: "$135",
        exitPoint: "Payment Page"
      },
      category: "sales"
    }
  ];

  return (
    <div className="max-w-7xl mx-auto p-6">
      <div className="mb-6">
        <h2 className="text-2xl font-bold text-gray-900">Analytics Overview</h2>
        <span className="sr-only">Dashboard showing key performance metrics</span>
      </div>

      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        {metrics.map((metric) => (
          <div 
            key={metric.id}
            className="bg-white rounded-xl shadow-md overflow-hidden hover:shadow-lg transition-shadow"
          >
            <div className="p-6">
              <div className="flex items-center justify-between mb-4">
                <div className="flex items-center">
                  <img
                    src={metric.icon}
                    alt=""
                    className="w-10 h-10 rounded-lg"
                    aria-hidden="true"
                  />
                  <h3 className="ml-3 text-lg font-medium text-gray-900">
                    {metric.title}
                  </h3>
                </div>
                <span 
                  className={`px-2.5 py-1 rounded-full text-sm font-medium ${
                    metric.trend === 'up' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
                  }`}
                >
                  <span className="not-sr-only">{metric.change}</span>
                  <span className="sr-only">
                    {`${metric.trend === 'up' ? 'Increased by' : 'Decreased by'} ${metric.change} ${metric.timeRange}`}
                  </span>
                </span>
              </div>

              <div className="flex items-baseline">
                <p className="text-3xl font-semibold text-gray-900">{metric.value}</p>
                <p className="ml-2 text-sm text-gray-500">{metric.timeRange}</p>
              </div>

              <div className="mt-4">
                <div className="space-y-2">
                  {Object.entries(metric.details).map(([key, value]) => (
                    <div key={key} className="flex justify-between items-center">
                      <span className="text-sm text-gray-500 capitalize">
                        {key.replace(/([A-Z])/g, ' $1').toLowerCase()}
                      </span>
                      <span className="text-sm font-medium text-gray-900">{value}</span>
                      <span className="sr-only">
                        {`${key.replace(/([A-Z])/g, ' $1').toLowerCase()}: ${value}`}
                      </span>
                    </div>
                  ))}
                </div>
              </div>

              <div className="mt-6">
                <button
                  className="w-full bg-blue-50 text-blue-700 px-4 py-2 rounded-lg hover:bg-blue-100 transition-colors"
                  aria-label={`View detailed ${metric.title.toLowerCase()} analytics`}
                >
                  <span className="not-sr-only">View Details</span>
                  <span className="sr-only">
                    {`View detailed analytics for ${metric.title}`}
                  </span>
                </button>
              </div>
            </div>
          </div>
        ))}
      </div>

      <div className="mt-8 text-center">
        <button
          className="inline-flex items-center px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
          aria-label="Generate comprehensive analytics report"
        >
          <span className="not-sr-only">Generate Report</span>
          <span className="sr-only">Generate comprehensive analytics report with all metrics</span>
          <svg 
            className="ml-2 w-5 h-5" 
            fill="none" 
            stroke="currentColor" 
            viewBox="0 0 24 24"
          >
            <path 
              strokeLinecap="round" 
              strokeLinejoin="round" 
              strokeWidth={2} 
              d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" 
            />
          </svg>
        </button>
      </div>
    </div>
  );
};

export default AnalyticsDashboard;

Best Practices

Maintain Design Consistency

When visually hiding content for accessibility, it's essential to apply these utilities in a predictable manner across your entire project. Establish a convention for when and where to use sr-only elements, ensuring they remain meaningful and do not lead to fragmented or disjointed experiences for screen reader users. For example, consistently using sr-only for supplementary descriptions or navigational landmarks.

Document and standardize the use of sr-only in a project's design system or component library. This approach ensures that all team members adhere to best practices and maintain a clear, accessible structure throughout the application.

Optimize for Reusability

Creating reusable components with sr-only ensures accessibility is built into the design from the start, making interfaces more scalable and maintainable. Instead of manually applying sr-only to every instance where screen reader text is needed, developers can integrate it into components by default. This approach ensures consistency and reduces the risk of missing essential accessibility features.

For example, an IconButton component can automatically include a visually hidden label alongside the icon, allowing screen reader users to understand its purpose without affecting the visual design. Similarly, form input components can have sr-only labels built-in, preventing accessibility issues caused by missing visible labels.

Accessibility Considerations

Enhance Readability and Navigability

Applying sr-only correctly can significantly improve the readability and navigability of a webpage for screen reader users. Hidden content should provide essential context or descriptions that enhance comprehension without overwhelming the user with redundant information. For example, visually hiding additional details for form fields with sr-only ensures that screen reader users receive clear guidance without interfering with the visual hierarchy.

Proper structure and semantic HTML are also crucial when incorporating sr-only elements. Placing visually hidden descriptions near their respective interactive elements ensures that assistive technologies announce them correctly. Additionally, avoid placing sr-only content in unrelated sections, as this can cause confusion and disrupt the flow of information.

Support Accessible Interactive Elements

When designing accessible user interfaces, it's essential to provide clear context for interactive elements like buttons, links, and form fields. While sighted users rely on visual cues such as icons, colors, and placement, screen reader users depend on textual descriptions to navigate effectively. The sr-only utility in Tailwind CSS plays a crucial role in ensuring accessibility by adding hidden text that is available only to assistive technologies.

A common use cases for sr-only is in icon-only buttons. For example, a button with just a magnifying glass icon typically represents a search function. However, without additional context, a screen reader user may not understand its purpose. By adding an sr-only label, you can provide clarity without altering the visible design.