A 100% API-compatible drop-in replacement for React Admin using ShadCN UI - Modern, beautiful admin interfaces with native mongo.do integration.
React Admin's architecture is excellent - the problem is MUI's dated aesthetics and heavy runtime. Shadmin preserves the API, replaces the rendering layer:
- Drop-in Replacement - Just change your import, keep your existing code
- Modern Aesthetics - ShadCN components with Tailwind CSS v4 and CSS variables
- Native mongo.do Integration - Zero-config data layer for edge deployment
- Lightweight Runtime - No MUI JSS overhead, just utility classes
- Copy-Paste Ownership - Components live in your codebase, fully customizable
- React 19 Ready - Built on the latest React with TanStack Query
// Before (React Admin)
import { Admin, Resource, List, Datagrid, TextField } from 'react-admin'
// After (Shadmin) - Just change the import!
import { Admin, Resource, List, Datagrid, TextField } from 'shadmin'| Component | Description |
|---|---|
<Admin> |
Root provider orchestration with routing, data, and auth providers |
<Resource> |
CRUD route registration for a data resource |
<List> |
List view with pagination, sorting, and filtering |
<Datagrid> |
Data table powered by TanStack Table |
<Create> |
Create form wrapper with record creation |
<Edit> |
Edit form wrapper with record update |
<Show> |
Read-only detail view |
<SimpleForm> |
Form container with validation |
<TabbedForm> |
Multi-tab form layout |
| Field | Description |
|---|---|
TextField |
Renders text values |
NumberField |
Formatted numbers via Intl.NumberFormat |
DateField |
Formatted dates via Intl.DateTimeFormat |
BooleanField |
Badge or icon for boolean values |
EmailField |
Clickable mailto link |
UrlField |
External link |
ReferenceField |
Fetches and renders related records |
ReferenceManyField |
Renders a list of related records |
ReferenceArrayField |
Renders an array of references |
ArrayField |
Iterator for array values |
FunctionField |
Custom render function |
| Input | Description |
|---|---|
TextInput |
Standard text input with label |
NumberInput |
Numeric input with formatting |
PasswordInput |
Password input with visibility toggle |
DateTimeInput |
Date and time picker |
SelectInput |
Dropdown selection |
SelectArrayInput |
Multi-select dropdown |
BooleanInput |
Switch toggle |
RadioButtonGroupInput |
Radio button group |
CheckboxGroupInput |
Checkbox array |
| Component | Description |
|---|---|
Layout |
Main layout with sidebar |
AppBar |
Top navigation bar |
Menu |
Sidebar navigation |
MenuItem |
Menu item with icon |
SubMenu |
Collapsible submenu |
| Method | Description |
|---|---|
getList |
Fetch a list with pagination, sorting, filtering |
getOne |
Fetch a single record by ID |
getMany |
Fetch multiple records by IDs |
getManyReference |
Fetch related records |
create |
Create a new record |
update |
Update a single record |
updateMany |
Update multiple records |
delete |
Delete a single record |
deleteMany |
Delete multiple records |
npm install shadminimport { Admin, Resource, List, Datagrid, TextField, DateField } from 'shadmin'
import { createMondoDataProvider } from 'shadmin-db'
import { MongoClient } from 'mongo.do'
// Connect to mongo.do
const client = new MongoClient('https://your-worker.workers.dev')
const dataProvider = createMondoDataProvider({ client, database: 'myapp' })
// Define your list view
const PostList = () => (
<List>
<Datagrid>
<TextField source="title" />
<TextField source="author" />
<DateField source="createdAt" />
</Datagrid>
</List>
)
// Create your admin
export const App = () => (
<Admin dataProvider={dataProvider}>
<Resource name="posts" list={PostList} />
<Resource name="users" list={UserList} />
</Admin>
)npx create-shadmin my-admin
cd my-admin
npm run devA complete, copy-pastable ToDo application in under 100 lines:
import {
Admin,
Resource,
List,
Datagrid,
TextField,
BooleanField,
Create,
Edit,
SimpleForm,
TextInput,
BooleanInput,
DataProvider,
} from 'shadmin'
// In-memory mock data provider
let todos = [
{ id: 1, title: 'Learn Shadmin', completed: true },
{ id: 2, title: 'Build an admin panel', completed: false },
{ id: 3, title: 'Deploy to production', completed: false },
]
let nextId = 4
const dataProvider: DataProvider = {
getList: async () => ({ data: todos, total: todos.length }),
getOne: async (resource, { id }) => ({
data: todos.find((t) => t.id === Number(id))!,
}),
getMany: async (resource, { ids }) => ({
data: todos.filter((t) => ids.includes(t.id)),
}),
getManyReference: async () => ({ data: [], total: 0 }),
create: async (resource, { data }) => {
const newTodo = { ...data, id: nextId++ }
todos.push(newTodo)
return { data: newTodo }
},
update: async (resource, { id, data }) => {
const index = todos.findIndex((t) => t.id === Number(id))
todos[index] = { ...todos[index], ...data }
return { data: todos[index] }
},
updateMany: async (resource, { ids, data }) => {
ids.forEach((id) => {
const index = todos.findIndex((t) => t.id === Number(id))
todos[index] = { ...todos[index], ...data }
})
return { data: ids }
},
delete: async (resource, { id }) => {
const deleted = todos.find((t) => t.id === Number(id))!
todos = todos.filter((t) => t.id !== Number(id))
return { data: deleted }
},
deleteMany: async (resource, { ids }) => {
todos = todos.filter((t) => !ids.includes(t.id))
return { data: ids }
},
}
// List view with Datagrid
const TodoList = () => (
<List>
<Datagrid rowClick="edit">
<TextField source="id" />
<TextField source="title" />
<BooleanField source="completed" />
</Datagrid>
</List>
)
// Create form
const TodoCreate = () => (
<Create>
<SimpleForm>
<TextInput source="title" />
<BooleanInput source="completed" defaultValue={false} />
</SimpleForm>
</Create>
)
// Edit form
const TodoEdit = () => (
<Edit>
<SimpleForm>
<TextInput source="title" />
<BooleanInput source="completed" />
</SimpleForm>
</Edit>
)
// App entry point
export default function App() {
return (
<Admin dataProvider={dataProvider}>
<Resource
name="todos"
list={TodoList}
create={TodoCreate}
edit={TodoEdit}
/>
</Admin>
)
}Run it:
npm install shadmin
# Add the above code to your App.tsx and start your dev serverimport { List, Datagrid, TextField, DateField, SearchInput, FilterButton } from 'shadmin'
const PostFilters = [
<SearchInput source="q" alwaysOn />,
<SelectInput source="status" choices={[
{ id: 'draft', name: 'Draft' },
{ id: 'published', name: 'Published' },
]} />,
]
const PostList = () => (
<List
filters={PostFilters}
sort={{ field: 'createdAt', order: 'DESC' }}
perPage={25}
>
<Datagrid>
<TextField source="title" />
<TextField source="author" />
<DateField source="createdAt" />
<BooleanField source="published" />
</Datagrid>
</List>
)import { Create, Edit, SimpleForm, TextInput, DateTimeInput, SelectInput } from 'shadmin'
const PostCreate = () => (
<Create>
<SimpleForm>
<TextInput source="title" validate={required()} />
<TextInput source="body" multiline rows={5} />
<SelectInput source="status" choices={[
{ id: 'draft', name: 'Draft' },
{ id: 'published', name: 'Published' },
]} />
</SimpleForm>
</Create>
)
const PostEdit = () => (
<Edit>
<SimpleForm>
<TextInput source="title" validate={required()} />
<TextInput source="body" multiline rows={5} />
<DateTimeInput source="publishedAt" />
</SimpleForm>
</Edit>
)import { Edit, TabbedForm, FormTab, TextInput, ReferenceArrayInput } from 'shadmin'
const PostEdit = () => (
<Edit>
<TabbedForm>
<FormTab label="Content">
<TextInput source="title" />
<TextInput source="body" multiline />
</FormTab>
<FormTab label="Metadata">
<DateTimeInput source="publishedAt" />
<ReferenceArrayInput source="tags" reference="tags" />
</FormTab>
</TabbedForm>
</Edit>
)import { Show, SimpleShowLayout, TextField, DateField, ReferenceField, ReferenceManyField, Datagrid } from 'shadmin'
const PostShow = () => (
<Show>
<SimpleShowLayout>
<TextField source="title" />
<ReferenceField source="authorId" reference="users">
<TextField source="name" />
</ReferenceField>
<DateField source="createdAt" />
<ReferenceManyField reference="comments" target="postId" label="Comments">
<Datagrid>
<TextField source="body" />
<DateField source="createdAt" />
</Datagrid>
</ReferenceManyField>
</SimpleShowLayout>
</Show>
)import { DataProvider } from 'shadmin'
const myDataProvider: DataProvider = {
getList: async (resource, params) => {
const { page, perPage } = params.pagination
const { field, order } = params.sort
const response = await fetch(`/api/${resource}?page=${page}&perPage=${perPage}&sort=${field}&order=${order}`)
const { data, total } = await response.json()
return { data, total }
},
getOne: async (resource, params) => {
const response = await fetch(`/api/${resource}/${params.id}`)
const data = await response.json()
return { data }
},
// ... implement other methods
}+-----------------------------------------------------------------------------+
| Your App |
| <Admin dataProvider={mondoProvider} authProvider={...}> |
| <Resource name="users" list={UserList} edit={UserEdit} /> |
| </Admin> |
+-----------------------------------------------------------------------------+
| SHADMIN LAYER |
| +-----------------------------------------------------------------------+ |
| | Components (React Admin API-compatible, ShadCN-rendered) | |
| | * Admin, Resource, List, Datagrid, Create, Edit, Show | |
| | * Field components (TextField, DateField, ReferenceField...) | |
| | * Input components (TextInput, DateInput, SelectInput...) | |
| | * Layout (Sidebar, AppBar, Breadcrumb) | |
| +-----------------------------------------------------------------------+ |
| +-----------------------------------------------------------------------+ |
| | Contexts & Hooks (identical to React Admin) | |
| | * RecordContext, ListContext, FormContext, ResourceContext | |
| | * useRecordContext, useDataProvider, useNotify, useRedirect | |
| +-----------------------------------------------------------------------+ |
| +-----------------------------------------------------------------------+ |
| | DataProvider Interface (9 methods, identical signature) | |
| | * getList, getOne, getMany, getManyReference | |
| | * create, update, updateMany, delete, deleteMany | |
| +-----------------------------------------------------------------------+ |
+-----------------------------------------------------------------------------+
| MONGO.DO DATA LAYER (Optional) |
| +-----------------------------------------------------------------------+ |
| | mondoDataProvider (native integration) | |
| | * Translates DataProvider calls -> mongo.do RPC | |
| | * Optimistic updates with change stream sync | |
| | * Connection pooling & request deduplication (built into mongo.do) | |
| +-----------------------------------------------------------------------+ |
+-----------------------------------------------------------------------------+
| Package | Description |
|---|---|
shadmin |
Core components, hooks, and contexts |
shadmin-db |
mongo.do DataProvider integration |
create-shadmin |
CLI scaffolding tool |
import { Admin, Resource } from 'shadmin'
import { createMondoDataProvider } from 'shadmin-db'
import { MongoClient } from 'mongo.do'
const client = new MongoClient('https://your-worker.workers.dev')
const dataProvider = createMondoDataProvider({
client,
database: 'myapp',
})
const App = () => (
<Admin dataProvider={dataProvider}>
<Resource name="posts" list={PostList} />
</Admin>
)import { Admin, Resource } from 'shadmin'
const authProvider = {
login: async ({ username, password }) => {
const response = await fetch('/api/auth/login', {
method: 'POST',
body: JSON.stringify({ username, password }),
})
if (!response.ok) throw new Error('Login failed')
const { token } = await response.json()
localStorage.setItem('token', token)
},
logout: async () => {
localStorage.removeItem('token')
},
checkAuth: async () => {
if (!localStorage.getItem('token')) throw new Error('Not authenticated')
},
checkError: async (error) => {
if (error.status === 401) {
localStorage.removeItem('token')
throw new Error('Session expired')
}
},
getPermissions: async () => {
return localStorage.getItem('permissions')
},
}
const App = () => (
<Admin dataProvider={dataProvider} authProvider={authProvider}>
<Resource name="posts" list={PostList} />
</Admin>
)import { Admin, Resource, Layout, Menu, MenuItem, AppBar } from 'shadmin'
const CustomMenu = () => (
<Menu>
<MenuItem to="/posts" primaryText="Posts" leftIcon={<FileIcon />} />
<MenuItem to="/users" primaryText="Users" leftIcon={<UserIcon />} />
</Menu>
)
const CustomLayout = (props) => (
<Layout {...props} menu={CustomMenu} appBar={AppBar} />
)
const App = () => (
<Admin dataProvider={dataProvider} layout={CustomLayout}>
<Resource name="posts" list={PostList} />
</Admin>
)| Category | Technology |
|---|---|
| Runtime | React 19, TypeScript 5.7 |
| Build | Vite, vite-plugin-dts |
| Styling | Tailwind CSS v4, CSS Variables |
| Components | ShadCN UI, Radix UI |
| Forms | react-hook-form, Zod |
| Tables | TanStack Table |
| Routing | React Router v7 |
| State | TanStack Query |
| Testing | Vitest, React Testing Library, Playwright |
| Visual Testing | Storybook 8, Chromatic |
| Package Manager | pnpm workspaces |
# Clone the repository
git clone https://github.com/nathanclevenger/shadmin.git
cd shadmin
# Install dependencies
pnpm install
# Run all packages in dev mode
pnpm dev
# Run tests
pnpm test
# Run tests with coverage
pnpm test --coverage
# Run Storybook
pnpm storybook
# Build all packages
pnpm build
# Type check
pnpm typecheck
# Lint
pnpm lint
# Format code
pnpm format# Unit tests
pnpm test
# Watch mode
pnpm test:watch
# E2E tests with Playwright
pnpm test:e2e
# Visual regression with Chromatic
pnpm chromaticpackages/
shadmin/ # Main package
src/
components/
core/ # Admin, Resource, CoreAdminContext
list/ # List, Datagrid, Pagination, Filters
detail/ # Create, Edit, Show
form/ # SimpleForm, TabbedForm, Toolbar
field/ # TextField, DateField, ReferenceField...
input/ # TextInput, SelectInput, ReferenceInput...
layout/ # Layout, Sidebar, AppBar, Menu
auth/ # Login, Logout, AuthRequired
contexts/ # RecordContext, ListContext, FormContext...
hooks/ # useGetList, useCreate, useNotify...
types/ # TypeScript interfaces
utils/ # Shared utilities
shadmin-db/ # mongo.do integration
src/
data-provider.ts # createMondoDataProvider
create-shadmin/ # CLI scaffolding
src/
index.ts # CLI entry point
templates/ # Project templates
MIT - see LICENSE
Contributions welcome! Please open an issue or submit a pull request.
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Write tests following TDD (RED -> GREEN -> REFACTOR)
- Add Storybook stories for new components
- Ensure all tests pass (
pnpm test) - Submit a pull request
Built for the edge. Designed for AI. Styled with taste.