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

@entropy-softworks/curbcut

v2026.9.10

Published

Reusable React Native accessibility primitives: language selection, accessibility preferences, and the providers that carry them through an Expo app

Readme

curbcut

Reusable React Native accessibility primitives for Entropy Softworks apps.

Named for the curb-cut effect: the ramps cut into sidewalks after 1970s disability activism turned out to serve everyone pushing a stroller, a cart or a suitcase. Accessibility is not a concession to a minority — it is the work that makes a product better for all of its users. This package exists so that work is written once and inherited, rather than re-argued in every product.

npm install @entropy-softworks/curbcut

What is in it

| Surface | What it does | | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | A11yProvider / useA11y() | Holds the language choice and the accessibility preferences, persists them, and seeds them from the OS on a first run | | LanguageSetupScreen | Full first-run screen: search, the language wheel, an options drawer, Continue | | AccessibilitySettingsScreen | The same settings for an authenticated settings stack | | LanguagePicker / LanguageWheel | The wheel on its own — a snapping carousel whose middle row is the selection | | A11yOptionsPanel | Every preference as a row | | LanguageCoin | A language's flag, drawn as a coin, in react-native-svg | | useHaptics / useReduceMotion / useA11yTextStyle | The preferences, applied |

Quick start

Wrap the app once, naming the languages it actually has translations for:

import { A11yProvider, LANGUAGES } from "@entropy-softworks/curbcut";

const OFFERED = LANGUAGES.filter((l) => ["en-US", "es-ES", "fr", "ja"].includes(l.code));

export default function RootLayout() {
  return (
    <A11yProvider languages={OFFERED} fallbackLanguage="en-US">
      <Stack />
    </A11yProvider>
  );
}

Then put the setup screen in front of your own first question:

import { LanguageSetupScreen } from "@entropy-softworks/curbcut";

export default function AccessibilitySetup() {
  const router = useRouter();
  return <LanguageSetupScreen onContinue={() => router.replace("/where-to-store-your-data")} />;
}

And read the state anywhere:

const { preferences, language, t, textScaleFactor } = useA11y();

Tailwind / NativeWind

curbcut's components use NativeWind classes for layout, so the consumer's Tailwind build has to see its source. Add the resolved package path to content:

const curbcutSrc = path.join(
  fs.realpathSync(path.resolve(__dirname, "node_modules/@entropy-softworks/curbcut")),
  "src/**/*.{js,jsx,ts,tsx}"
);

module.exports = {
  presets: [require("@entropy-softworks/curbcut/tailwind-preset")],
  content: ["./app/**/*.{js,jsx,ts,tsx}", curbcutSrc],
};

realpathSync rather than a literal glob: in a workspace the package is hoisted to the monorepo root and ./node_modules/@entropy-softworks/curbcut/src matches nothing — silently, producing a build whose stylesheet is missing every class used only inside this package. The preset is optional if you already extend entropy-ui's; the two define the same core tokens.

Two design rules worth knowing before you extend it

No new native modules. Everything here is pure JS, core react-native, or a native module the consumer already has. The system locale comes from Intl and I18nManager, not expo-localization; the flags are hand-drawn SVG, not an icon set. The reason is delivery: a new native module changes the Expo fingerprint, so consumers on the fingerprint runtimeVersion policy can no longer reach their installed builds over the air. An accessibility package that costs a full rebuild to adopt is one that gets deferred to next quarter, every quarter.

The wheel is a real ScrollView. Not a pan gesture. A hand-rolled carousel would give better control over the physics and would also throw away everything a ScrollView gets for free — momentum, snapping, the platform scroll sound, and the part that actually matters here: a screen reader can page through its children and a switch-control user can move focus through them. An accessibility package cannot ship an inaccessible control as its centrepiece.

Language coverage

LANGUAGES carries 37 entries with a coin and both names for each. Two things about it:

  • It is a registry, not a policy. Pass the subset you have translations for. The default is the whole list, which is right for a demo and a lie in a shipping app.
  • curbcut's own strings — the search placeholder, the option labels, Continue — are translated into 15 locales and fall back to English for the rest. That gap is deliberate: the wheel has to be readable before a language is chosen, and an English fallback is a known quantity where a machine-guessed translation is not. Adding a locale is a merge request with a native speaker on it.

Arabic has no flag coin. It is official in more than twenty states and picking one of their flags to mean "Arabic" is a political statement rather than a localisation decision, so it gets a script-glyph coin. Any future language in the same position should do the same.

The flags themselves are drawn to be legible at 34 points, not to be exact. Where a charge cannot survive that size it is simplified or dropped, and every such decision is named in a comment at the component that makes it — see src/flags/custom.tsx.

Preferences curbcut cannot apply for you

reduceMotion, haptics and textScale are honoured by curbcut's own components. highContrast and colorFilter are recorded only — mapping them onto a palette needs the app's colours, so the app has to do it:

const { preferences } = useA11y();
const palette = preferences.highContrast ? HIGH_CONTRAST : DEFAULT;

Development

npm install
npm run check-all                       # type-check, lint, format, test
pwsh scripts/pipeline.ps1 -Stage test   # the gate CI runs

See CONTRIBUTING.md. Security policy in SECURITY.md.

License

MIT — see LICENSE.