designed-by-ai/skillsCheck passed
SKILL DETAIL
design-mobile-apps
designed-by-ai/skills/design-mobile-apps
This skill uses Sleek's REST API to create projects, design screens from a brief, and fetch their HTML and renders. It covers both the design pass (the create, chat, and screenshot loop) and the implementation pass (version selection, native conversion, working features, and acceptance checks), supporting implementation of designs in React Native or HTML. It requires the SLEEK_API_KEY environment variable.
Installs · 20,251View source
Installation
npx skills add https://github.com/designed-by-ai/skills --skill design-mobile-apps
Skill files
SKILL.md
Last synced · Oct 26, 2026
designing.md›
# Designing
Request and response shapes go beyond the fields named here; read the [API reference](endpoints.md) when you need a field this file doesn't name.
## 1. Create a project
Create a project with `POST /api/v1/projects` if one doesn't exist yet.
Each project has its own theme, style, and design system, so a project holds one style direction at a time. Even so, keep design variations in one project by default: project count is plan-capped (free 1, Starter 5), and `POST /api/v1/projects` answers a capped account with `403 FORBIDDEN` at `projects_limit`. That 403 carries no `data.url`, so name the cap and the upgrade to the user yourself. Spread variations across projects only when the user asks to see them side by side — and check the headroom with `GET /api/v1/projects` before you start.
## 2. Send a chat message
Send the request with `POST /api/v1/projects/:id/chat/messages`. Sleek plans screen content and layout from your message, and invents a visual style when you give it none. Send the full intent as a single message in the user's own terms: Sleek does the decomposition into screens itself and produces richer designs when given room to plan. If the user described specific screens, include those.
**Author a style direction**: write one whenever the user has given you anything to ground it in — reference images, apps they like, vibe adjectives, things to avoid — or whenever you're producing variations, one direction per variation. Pass the request through unchanged only when it's bare. A style direction is a single comprehensive paragraph, included in the message, covering mood (2–3 adjectives), color strategy (the logic, not hex codes), typography feel, layout philosophy, component style (radii, borders vs shadows, nav treatment), imagery and illustration style, and one or two distinctive details. Commit to a palette, a type direction, and an overall feel — anything that only sets a mood reads as a hint, not a direction. Put the personality in color, type, and imagery rather than in unusual layout or navigation.
Extend what the user gave you and keep every constraint they set. When they point at reference images or apps they like, study each one and carry what you take into the direction — Sleek only sees images passed as `imageUrls`, so for anything local the direction is how those references reach it. Borrow patterns, and leave the source's branding, content, and name behind.
Use a style direction or a `referenceId`, not both — a reference already carries a full style guide of its own.
**Seed a style with a reference**: Sleek curates a catalog of design references. When the user wants a specific look or asks for style options, list them with `GET /api/v1/references` (each has a `name` and `previewImageUrls` you can show) and pass the chosen id as `referenceId` on the first message to a project, so its style guide seeds the whole design.
**Identify your tool**: always send `source`, the slug of the tool making the request. The Sleek editor uses it to show the user who is designing while the run streams. Recognized values: `claude-code`, `claude`, `codex`, `chatgpt`, `cursor`, `openclaw`, `grok`. If your tool isn't listed, send a short kebab-case slug for it anyway (max 64 chars). Unrecognized values are fine and get a generic label.
**Watch it live**: runs render in the Sleek editor in real time. After sending the first message to a project, tell the user they can watch their screens being designed live in Sleek, and share the editor link: `https://sleek.design/project/:projectId`. Open a browser yourself only when the user asks you to.
**Polling**: chat messages are async by default: you get a `runId` and poll `GET /api/v1/projects/:id/chat/runs/:runId`. Start at a 2s interval, back off to 5s after 10s, and poll for up to **10 minutes**. Exit on `completed` or `failed`; if you can't read the status, stop and report that rather than counting it as "not done yet". After ten minutes, report the last known status and run ID. Elapsed time does not prove completion or release the project's run lock; don't start another attempt while its outcome is unknown. You can also use `?wait=true` for a blocking call (up to 300s; falls back to polling if it times out with `202`).
Save the request body, idempotency key, `runId` and `statusUrl` per project as soon as they are available. A `409` carries no active-run pointer and the API has no run-list endpoint. If the blocking run's ID is unknown, report the conflict and request its run ID or editor context; don't invent a polling URL or cancel an unidentified run.
A `failed` run can still carry a `result`: Sleek records the operations it applied before the failure. Read `result` on both terminal statuses and show the created or updated screens through step 3. Report the failure alongside the applied changes. Continue to implementation only when the user requested code.
**Editing a specific screen**: use `target.screenId` to direct changes to the right screen. Read it from the run's `result.operations` or the component list's `screenId` field, which can be null. It is distinct from the component ID; do not substitute that ID when no screen ID is returned.
**One run at a time**: keep one active run per project. Follow a known run to a terminal status before sending the next message; use the missing-context path above when its ID is unknown. If the user changes direction, cancel the identified run (see [Cancel Run](endpoints.md#chat-cancel-run)). Messages to different projects can run in parallel; use async polling when running them concurrently.
**Safe retries**: choose an `idempotency-key` header (≤255 chars) before sending a message. If the response is lost, resend the same payload with the same key to recover the original run. Reusing a key also returns its terminal failure: after `request_message_persist_failed`, or after the user resolves `out_of_credits`, a new logical attempt needs a new key. Read the existing run first; don't create a new attempt while its outcome is unknown.
## 3. Show the results
After every chat run that produces `screen_created` or `screen_updated` operations, **take screenshots and show them to the user** using `POST /api/v1/screenshots`. The step is done once the user has seen a **user shot** of every screen the run created or updated, and each run ends with the user having seen what it changed.
- **New screens**: one screenshot per screen + one combined screenshot of all screens in the project.
- **Updated screens**: one screenshot per affected screen.
Save screenshots in the project directory (not a temporary folder) so the user can easily view them.
**Review your own work with a review shot.** The user shot's phone-shaped framing is the point when you're showing a screen to the user, and the wrong framing for judging it: everything below the fold is cropped away. Re-shoot with `fullHeight: true`, one screen per request, when you're checking what a run actually produced.
Screenshot requests are independent, so issue them in parallel — the user shot and the review shot go out together, as do the shots for different screens. "One screen per request" governs what goes into each image, not how fast you send them. Back off only if you actually get a `429`.
**Confirm missing content.** Content that looks missing in a user shot can be below the fold. Review the full-height shot first. Before reporting something absent or asking Sleek to add it, check the component HTML from `GET /api/v1/projects/:id/components/:componentId` too: rendering can omit content past its height cap or inside collapsed sections and carousel slides. An element present in HTML can still be invisible to the user.
---
When the user asks for these screens as code, read [implementing.md](implementing.md) before writing the implementation.
endpoints.md›
# Endpoints
Full contract for every row in the quick-reference table in [SKILL.md](SKILL.md).
Every list endpoint accepts `limit` (1–100, default 50) and `offset` (≥0), and answers with `pagination.total` so you can page through everything.
## Projects
### List projects
```http
GET /api/v1/projects?limit=50&offset=0
Authorization: Bearer $SLEEK_API_KEY
```
Response `200`:
```json
{
"data": [
{
"id": "Nq4bZp8wLcT",
"name": "My App",
"slug": "my-app",
"createdAt": "2026-01-01T00:00:00Z",
"updatedAt": "..."
}
],
"pagination": { "total": 12, "limit": 50, "offset": 0 }
}
```
### Create project
```http
POST /api/v1/projects
Authorization: Bearer $SLEEK_API_KEY
Content-Type: application/json
{ "name": "My New App" }
```
Response `201`: `{ "data": ... }` with a single project.
A plan-capped account gets `403 FORBIDDEN` at `projects_limit` here, and that body carries no `data.url`.
### Get / Delete project
```http
GET /api/v1/projects/:projectId
DELETE /api/v1/projects/:projectId → 204 No Content
```
---
## Components
### List components
```http
GET /api/v1/projects/:projectId/components?limit=50&offset=0
Authorization: Bearer $SLEEK_API_KEY
```
Both list and get accept an optional `inlineIcons` query param (default `false`). When omitted, icons render as `<iconify-icon>` web components and the HTML pulls in the Iconify script, so leave it off by default. Pass `?inlineIcons=true` only when the consumer needs self-contained SVGs in the HTML (for example, importing into tools that don't run scripts).
Response `200`:
```json
{
"data": [
{
"id": "V1StGXR8Z5j",
"screenId": "7hSvA2kLpEr",
"name": "Hero Section",
"activeVersion": 3,
"versions": [
{
"id": "qkP2mN7bXsT",
"version": 1,
"code": "<!DOCTYPE html>...</html>",
"createdAt": "..."
}
],
"createdAt": "...",
"updatedAt": "..."
}
],
"pagination": { "total": 5, "limit": 50, "offset": 0 }
}
```
`activeVersion` can be null or unmatched. See [Which version to use](implementing.md#which-version-to-use) for code and screenshot selection. `id` is the component ID; nullable `screenId` addresses targeted chat edits.
### Get component
```http
GET /api/v1/projects/:projectId/components/:componentId
Authorization: Bearer $SLEEK_API_KEY
```
Response `200`: `{ "data": ... }` with a single component in the same shape as a list item.
---
## References
References are curated design styles from featured Sleek projects. They are world-readable: any valid API key can list them, no scope needed.
```http
GET /api/v1/references?limit=50&offset=0
Authorization: Bearer $SLEEK_API_KEY
```
Response `200`:
```json
{
"data": [
{
"id": "3fTkYw6RmQv",
"name": "Ember Fitness",
"previewImageUrls": ["https://.../screenshot.png"]
}
],
"pagination": { "total": 44, "limit": 50, "offset": 0 }
}
```
To use one, pass its `id` as `referenceId` on [Send Message](#chat-send-message).
---
## Chat: Send Message
```http
POST /api/v1/projects/:projectId/chat/messages?wait=false
Authorization: Bearer $SLEEK_API_KEY
Content-Type: application/json
idempotency-key: <optional, max 255 chars>
{
"message": { "text": "Add a pricing section with three tiers" },
"source": "claude-code",
"imageUrls": ["https://example.com/ref.png"],
"target": { "screenId": "7hSvA2kLpEr" },
"referenceId": "3fTkYw6RmQv"
}
```
| Field | Required | Notes |
| ------------------------ | -------- | ---------------------------------------------------------------------------------------- |
| `message.text` | Yes | 1+ chars, trimmed |
| `source` | Treat as required | Slug of the tool sending the request (see [step 2 of Designing](designing.md#2-send-a-chat-message)) |
| `imageUrls` | No | HTTPS URLs only; included as visual context. Sleek's servers fetch these URLs, so pass ones you're willing to have Sleek read |
| `target.screenId` | No | Edit a specific screen using a non-null `screenId` from run operations or the components list, not `componentId`; omit to let AI decide |
| `referenceId` | No | Seed the design style from a reference (see [References](#references)); invalid id → `400` |
| `?wait=true/false` | No | Sync wait mode (default: false) |
| `idempotency-key` header | No | Replay-safe re-sends |
### Response: async (default, `wait=false`)
Status `202 Accepted`. `result` and `error` are absent until the run reaches a terminal state.
```json
{
"data": {
"runId": "kR9dLm2XvBc",
"status": "queued",
"statusUrl": "/api/v1/projects/Nq4bZp8wLcT/chat/runs/kR9dLm2XvBc"
}
}
```
### Response: sync (`wait=true`)
Blocks up to **300 seconds**. Returns `200` when completed, `202` if timed out.
```json
{
"data": {
"runId": "kR9dLm2XvBc",
"status": "completed",
"statusUrl": "...",
"result": {
"assistantText": "I added a pricing section with...",
"operations": [
{
"type": "screen_created",
"screenId": "wG5xTn1PjHf",
"screenName": "Pricing",
"componentId": "V1StGXR8Z5j"
},
{
"type": "screen_updated",
"screenId": "7hSvA2kLpEr",
"componentId": "8rJvL4wYhKd"
},
{ "type": "theme_updated" }
]
}
}
}
```
`screenId` and `componentId` are distinct ids for the same screen: `screenId` addresses it in `target`, `componentId` addresses it in `GET .../components/:componentId` and in `componentIds` on a screenshot.
---
## Chat: Poll Run Status
```http
GET /api/v1/projects/:projectId/chat/runs/:runId
Authorization: Bearer $SLEEK_API_KEY
```
The response has the same `data` shape as send message: `result` when the run applied operations, `error` when `failed`.
```json
{
"data": {
"runId": "kR9dLm2XvBc",
"status": "failed",
"statusUrl": "...",
"error": { "code": "execution_failed", "message": "..." }
}
}
```
A `failed` run can carry **both** `error` and `result`: the server records the operations that were already applied before the failure. Read `result` on either terminal status.
**Run status lifecycle**: `queued` → `running` → `completed | failed`
---
## Chat: Cancel Run
```http
POST /api/v1/projects/:projectId/chat/runs/:runId/cancel
Authorization: Bearer $SLEEK_API_KEY
```
Marks a `queued` or `running` run as `failed` with error code `cancelled` and returns the updated run; already-finished runs are returned unchanged.
---
## Screenshots
Takes a snapshot of one or more rendered components.
```http
POST /api/v1/screenshots
Authorization: Bearer $SLEEK_API_KEY
Content-Type: application/json
{
"componentIds": ["V1StGXR8Z5j", "8rJvL4wYhKd"],
"projectId": "Nq4bZp8wLcT",
"format": "png",
"scale": 2,
"gap": 40,
"padding": 40,
"background": "transparent"
}
```
`componentIds` takes `componentId` values, which the `screenId` from a chat operation is not.
| Field | Default | Notes |
| --------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `format` | `png` | `png` or `webp` |
| `scale` | `2` | 1–3 (device pixel ratio) |
| `gap` | `40` | Pixels between components |
| `padding` | `40` | Uniform padding on all sides |
| `paddingX` | _(optional)_ | Horizontal padding; overrides `padding` for left/right when provided |
| `paddingY` | _(optional)_ | Vertical padding; overrides `padding` for top/bottom when provided |
| `paddingTop` | _(optional)_ | Top padding; overrides `paddingY` when provided |
| `paddingRight` | _(optional)_ | Right padding; overrides `paddingX` when provided |
| `paddingBottom` | _(optional)_ | Bottom padding; overrides `paddingY` when provided |
| `paddingLeft` | _(optional)_ | Left padding; overrides `paddingX` when provided |
| `background` | `transparent` | Any CSS color (hex, named, `transparent`) |
| `showDots` | `false` | Overlay a dot grid on the background; light dots on dark backgrounds and dark dots on light ones. No effect when `background` is `transparent` |
| `fullHeight` | `false` | Expand each frame to the height of its own content before capturing — this is what makes a review shot |
| `radius` | `48` | Squircle corner radius per component in pixels (integer ≥ 0); pass `0` for sharp corners |
| `componentVersionOverrides` | _(optional)_ | Map of `componentId` → `versions[i].id` to render at a pinned version instead of `activeVersion` (see [Pinned versions](implementing.md#pinned-versions)) |
| `themeVersionOverrides` | _(optional)_ | Map of `themeId` → `versions[i].id` to render with a pinned theme version (see [Pinned versions](implementing.md#pinned-versions)) |
Padding resolves per-side → axis → uniform: `paddingTop` falls back to `paddingY`, which falls back to `padding`.
Frames are capped at **4× the default frame height**, so a screen longer than that is still cut off at the bottom even with `fullHeight: true`. On a very long screen, treat the component HTML as the authority for what's below the cap.
Response: raw binary `image/png` or `image/webp` with `Content-Disposition: attachment`.
---
## Share Cards
Renders one shareable image from up to four screens — the social-post framing, distinct from the plain screenshot.
```http
POST /api/v1/share-cards
Authorization: Bearer $SLEEK_API_KEY
Content-Type: application/json
{
"componentIds": ["V1StGXR8Z5j", "8rJvL4wYhKd"],
"projectId": "Nq4bZp8wLcT",
"layout": "stack",
"background": "midnight",
"title": "Ember Fitness"
}
```
| Field | Default | Notes |
| --------------------------- | ---------- | -------------------------------------------------------------- |
| `componentIds` | _required_ | 1–4 components |
| `projectId` | _required_ | Project the components belong to |
| `layout` | `stack` | `stack` or `row` |
| `background` | `midnight` | `midnight`, `ember`, `paper`, `violet`, `ocean` |
| `title` | _(optional)_ | Trimmed, max 80 chars |
| `componentVersionOverrides` | _(optional)_ | Same pinning semantics as screenshots |
| `themeVersionOverrides` | _(optional)_ | Same pinning semantics as screenshots |
Response: raw binary `image/png`. Share cards and screenshots share one render budget — see the `429` row in [SKILL.md](SKILL.md#error-shapes).
---
## Device: Start
```http
POST /api/v1/device/start
Content-Type: application/json
{ "source": "claude-code" }
```
No auth. `source` is optional (1–64 chars) and drives the `<source> wants to connect` copy the user sees on the approval page.
Response `201`:
```json
{
"data": {
"deviceCode": "...",
"userCode": "ABCD-1234",
"verificationUrl": "https://sleek.design/...",
"expiresIn": 900,
"interval": 5
}
}
```
## Device: Poll
```http
POST /api/v1/device/poll
Content-Type: application/json
{ "deviceCode": "..." }
```
No auth. `data.status` is one of `pending`, `expired`, or `approved` — and only the `approved` variant carries `key`, `keyId` and `name`. It is handed over exactly once; a poll on an already-claimed code answers `404 NOT_FOUND` / `Unknown device code`, which is indistinguishable from a code that never existed. The full flow is in [SKILL.md](SKILL.md#prerequisites-api-key).
implementing.md›
# Implementing Designs
When the user wants the designs built as code rather than previewed, **always fetch the component HTML code** and build from it; the screenshots confirm the result.
Use `GET /api/v1/projects/:id/components/:componentId` to fetch each screen's code. For a new run, get `componentId` from `result.operations`. For an existing project, list its components, paging through `pagination.total`; each item supplies `id` for HTML/screenshots and a nullable `screenId` for targeted edits. Use a returned screen ID when present; don't substitute the component ID when it is null.
Component code can be large. Fetch it with a shell command that writes the response body straight to disk, so the code goes to a file without passing through your text output.
## Which version to use
Each component carries a `versions[]` array and an `activeVersion`, a **nullable** number. **By default, use the entry where `versions[i].version === activeVersion`**. If `activeVersion` is `null` or has no matching entry, select the **highest numeric `version`** for implementation. Pass that selected entry's `id` in `componentVersionOverrides` for its screenshots too: the renderer can otherwise produce an empty frame. If there are no versions, stop and report that there is no code to implement.
If the user's prompt pins specific versions, follow those instead (see [Pinned versions](#pinned-versions) below).
## Pinned versions
The user's prompt may include a pin block telling you to implement specific historical versions instead of the current ones, like this:
```
... at this exact state instead of the project's current version:
- component V1StGXR8Z5j: version qkP2mN7bXsT
- component 8rJvL4wYhKd: version zC5tRb9WqNm
- theme MfD3xQ6nJyU: version 7hSvA2kLpEr
```
When you see a pin block, implement those exact versions instead of `activeVersion`. Components not named in the pin block continue to use their active version. Theme ids surface only inside pin blocks; this skill exposes no separate endpoint to enumerate them.
**Historical themes are a separate limit.** Component HTML inlines the active project theme, even for historical component versions. Screenshot `themeVersionOverrides` do not change that HTML, and the public API has no theme-version read or HTML theme override. Preserve supplied theme pins for preview and use the screenshot as a visual reference, but do not claim an exact implementation from active-theme HTML. Exact implementation of a pinned historical theme needs its supplied tokens and fonts or an API export that resolves that theme; report this dependency rather than silently substituting the active theme.
### Fetching the right code
For each pinned component, find the entry in `versions[]` whose `versions[i].id` matches the given version id and use its `code`. Match on `id`, the string; `version` is the numeric index, and a pinned component takes its code from that matched entry alone.
If the requested version is absent, report the unavailable pin and stop that export. Do not substitute another version. The screenshot API silently falls back to active versions for invalid overrides, so a successful image response alone does not verify a checkpoint.
### Screenshots of pinned versions
Pass `componentVersionOverrides` and `themeVersionOverrides` to `POST /api/v1/screenshots`:
```json
{
"componentIds": ["V1StGXR8Z5j"],
"projectId": "Nq4bZp8wLcT",
"componentVersionOverrides": { "V1StGXR8Z5j": "qkP2mN7bXsT" },
"themeVersionOverrides": { "MfD3xQ6nJyU": "7hSvA2kLpEr" }
}
```
Keys are component / theme public ids; values are the corresponding `versions[i].id`. Entities missing from a map fall back to their active version. Include the override maps whenever the prompt specified pinned versions.
## HTML prototypes
The component `code` is a complete HTML document. Save it directly to a `.html` file. No build step needed.
## Native frameworks
Each component document is a **mockup**: a picture of a working screen, drawn in HTML because HTML is what a design tool renders.
- **Structure, layout and styling are the spec.** Element hierarchy, flex direction, spacing, sizing, colors, typography, radii, image URLs and icon names — reproduce these exactly.
- **Content is placeholder.** The names, numbers, copy, avatars, list items and "active" states are stand-ins that make the screen legible. Rebuild the container and feed it real state, props or API data — see [Build the feature, not the mockup](#build-the-feature-not-the-mockup).
- **The mockup draws the app's chrome** — tab bar, header, drawer — inside every screen that shows it, because each screen is a standalone document. Chrome belongs to the app's navigation, so build it there once and let each screen render its own content.
- **Screenshots are the visual target.** Check each built screen against its **review shot**; a user shot crops the page and never proves something is missing.
The HTML tells you _how_ to build it; the screenshot tells you _what_ it should look like; the app supplies the data.
### Icons
Sleek uses [Iconify](https://iconify.design) icons in the format `prefix:name` (e.g. `ph:heart-fill`, `solar:heart-bold`, `lucide:settings`). Sleek's design prompt steers toward four sets — **Phosphor** (`ph:`), **Solar** (`solar:`), **MingCute** (`mingcute:`) and **Hugeicons** (`hugeicons:`) — while allowing any Iconify set, so read the prefixes out of the HTML rather than assuming them.
**Use the exact icons from the HTML code** — same set, same name. Matching icons is what carries design fidelity.
When implementing icons:
1. **Check whether the project already has an icon system** that resolves the same names. On a repo that already has `@expo/vector-icons`, `mdi:*` is a free rename: Iconify's `mdi` and the package's `MaterialCommunityIcons` are the same upstream set with matching glyph names, so `mdi:account-check` is `<MaterialCommunityIcons name="account-check" />`. The rest take the Iconify route below — `solar:*` and `hugeicons:*` have no counterpart in the package at all, and `material-symbols:*` is a different Google generation from its `MaterialIcons`. For any other prefix, check the package's set list before assuming a match.
2. **Otherwise, fetch the SVGs from the Iconify API and embed them in the code:**
```
GET https://api.iconify.design/{prefix}/{name}.svg
```
Example: `https://api.iconify.design/solar/heart-bold.svg`
Collect all icon names from the HTML, fetch their SVGs, and save them as static assets or string constants in the codebase. For **React Native / Expo**, render them with `react-native-svg`'s `SvgXml` component, which works in Expo Go with no additional native dependencies.
If the Iconify endpoint is unavailable, obtain the same prefix/name from its official `@iconify-json/<prefix>` data package and embed only the icons used. Preserve the SVG geometry and viewBox; replacing the set changes the design.
### Fonts
For the current theme, the `<head>` `<link>` tags give you the font **families**. For a pinned historical theme, use the supplied theme's families instead. The links request each family's entire published weight range (or a 100–900 fallback for a family Sleek doesn't recognise), so the weights in those URLs describe what Google Fonts can serve, not what the design uses. The weights actually used are in the body's `font-*` Tailwind classes. Read those weights and bundle only the faces used.
Resolve body font classes through the theme's family tokens before bundling. An exported alias such as `--font-font-heading` does not define Tailwind's `font-heading` utility, even when the intended `--font-heading` family is present. Correct malformed aliases when porting and match the family rendered in the Sleek screenshot. If the intended token and screenshot still disagree, record the discrepancy rather than silently choosing one.
### Design tokens
Every component document inlines the project's theme in the `<style type="text/tailwindcss">` block in `<head>`: a `:root` rule holding the raw values (`--background`, `--foreground`, `--primary`, `--border`, `--radius`, `--font-*`, …) plus an `@theme inline` block naming them for Tailwind. The same theme is inlined into every screen of a project, so port it once into a single token module and have every component read from that module.
Two of those values need work on the way across:
- **`--radius` is a base, not a value.** The document derives `--radius-xs … --radius-4xl` from it with `calc()`; resolve the arithmetic to numbers.
- **`--shape` is always present**, valued `round` or `squircle` — test the value, not its presence. `squircle` turns on CSS `corner-shape` and multiplies every radius by `--shape-multiplier` where the browser supports it. React Native has neither, so use plain `borderRadius` and match the rounding the screenshot shows.
Colors can be hex, `rgb()` or `oklch(...)`. Preserve supported values and convert unsupported formats once; the file below covers each styling branch.
---
## React Native (Expo)
Sleek renders web HTML with Tailwind v4 classes (the document loads `@tailwindcss/browser@4` — read the `<head>` to confirm). Several web defaults land differently in React Native, and the failures below are **silent**: the app compiles, runs, and is wrong. Work through them before the first component.
**Styling.** Use the user's requested styling system, otherwise preserve the system used by the feature being edited. Dependencies and source files provide evidence; read the corresponding reference:
| Evidence | Read |
| --------------------------------------------- | ---------------------------------------------- |
| Nativewind classes in the feature, or Nativewind requested | [styling/nativewind.md](styling/nativewind.md) |
| Uniwind classes in the feature, or Uniwind requested | [styling/uniwind.md](styling/uniwind.md) |
| `StyleSheet.create` in the feature, or a new app without a library preference | [styling/stylesheet.md](styling/stylesheet.md) |
Each file carries its own token conversion, class syntax and configuration. For a repo with no styling library and a user with no preference, use StyleSheet: it needs no build configuration, so it cannot be misconfigured into silently doing nothing.
### Flex direction
The most common silent failure. On the web `display: flex` lays children out in a **row**; in React Native the default `flexDirection` is `'column'`. Sleek writes the web idiom, so `class="flex items-center gap-3"` is a horizontal row that renders as a vertical stack until you say `flexDirection: 'row'` (or `flex-row`).
`flex-col` is already the default and needs nothing; `flex-1` maps to `flex: 1`; `inline-flex` is dropped by the style engine with a value warning, which lands on the same layout as `flex` because React Native has no inline mode. Sweep every `flex` in the source and give each one an explicit direction.
### Text
Every string renders inside `<Text>` — including strings hiding in a `&&` branch, a ternary or a stray `{' '}`. A bare string under a `<View>` fails **silently**: React Native's New Architecture routes `Text strings must be rendered within a <Text> component.` through `console.error`, so it appears in a development build's console and nowhere else. A production build carries no such message — the string is simply missing from the screen, and nothing throws.
`<span>` and `<p>` both become `<Text>`, and spans nested inside a paragraph become nested `<Text>`. Text styling (`fontSize`, `color`, `fontWeight`, `lineHeight`) sits on the `<Text>` itself, since it does not cross a `View` boundary.
### Insets
The mockup has no notch, status bar or home indicator: it renders edge to edge inside a fixed frame. On a device, an unguarded screen draws its header under the status bar and its bottom bar under the home indicator or the Android navigation bar — Expo SDK 54 and later make Android edge-to-edge the default, so Android content draws under the system bars just as iOS content does. Inset with **`react-native-safe-area-context`** — React Native core exports a `SafeAreaView` of its own that only insets on iOS, so import from the package — and put a `SafeAreaProvider` at the app root. Native modal or route roots can need their own provider when `react-native-screens` creates a separate view hierarchy; check the presented screen's actual insets. See the [provider documentation](https://appandflow.github.io/react-native-safe-area-context/api/safe-area-provider/).
Inset each edge of each screen exactly once:
- A native-stack navigator header insets the **top only**. A screen under one still owns its bottom edge, so unless a tab bar or other navigator chrome covers it, give the screen `edges={['bottom']}` or bottom padding from `useSafeAreaInsets()`.
- A headerless screen, including a headerless modal, owns its top and bottom insets: `SafeAreaView` around the screen, or `useSafeAreaInsets()` when you need the numbers — a header you drew yourself, a floating or absolutely positioned bottom bar, a sticky CTA, or content that scrolls under the status bar while its padding respects it. On modal routes with navigator chrome, inset only the uncovered edges.
- `edges` narrows a `SafeAreaView` to the sides that still need guarding, which is how a screen keeps the navigator header and insets the bottom alone.
### Shadows
`boxShadow` takes the mockup's CSS shadow across 1:1 on both platforms — `boxShadow: '0px 4px 12px rgba(0,0,0,0.15)'`, or the array-of-objects form. It is New Architecture only, which is the only architecture React Native ships from 0.82, so it is the default choice and the one that keeps Android matching the design.
Legacy fallback, for a project on an older React Native or still on the old architecture: iOS reads `shadowColor`, `shadowOffset`, `shadowOpacity` and `shadowRadius`; Android reads `elevation` and derives its own blur and offset from it, so the design's exact shadow lands as an approximation there. Set both, and give the shadowed view a `backgroundColor` so `elevation` draws at all.
### Gradients and hover
- **Gradients.** Use `expo-linear-gradient` for linear gradients, or preserve an existing native gradient implementation supported by the installed React Native version. Check native API support before porting CSS gradient strings directly.
- **Hover.** `hover:` variants compile but never fire on a touch device. Carry the intent to the press state instead: `Pressable`'s `({ pressed })` style callback, or the `active:` variant.
### Infer the route tree first
Do this before implementing any screen. Each Sleek screen is a standalone document that draws the entire chrome, so the same tab bar is baked into every screen that shows one. Read the screen set as one app, derive the navigator from it, and let each screen render only its own content — the navigator owns the chrome and draws it once.
Preserve the app's existing navigation system. For a new Expo app with no navigator, use **expo-router**; in that branch, import each navigator from the path that actually exports it:
- `Stack` from `expo-router`.
- `Tabs` from `expo-router/js-tabs` on Router 57+. Older Router versions, including Router 6 on SDK 54, export `Tabs` from `expo-router` and may not have `js-tabs`. Check the installed package's exports before choosing the import; preserve the project's SDK unless an upgrade is part of the task.
- `Drawer` from `expo-router/drawer` — the root exports no `Drawer` at all. It also needs `react-native-reanimated`, `react-native-worklets` and `react-native-gesture-handler` (SDK 56+) installed alongside it.
Four signals carry the structure:
1. **The same bottom bar means Tab siblings** — those screens sit under one `_layout` sharing a single tab bar.
2. **A back arrow in the header means a Stack child** of wherever the screen is reached from.
3. **An overlay, sheet or dialog needs a presentation choice.** Use a modal route when it belongs in navigation history, or a screen-local component when it belongs to the current interaction. Match the existing app's behavior.
4. **A hamburger menu means a Drawer**, with its destinations as Drawer screens.
Worked example: Home, Search and Profile all show the same bottom bar → tab siblings. ProductDetail shows a back arrow → Stack child inside the Home tab. Settings is reached from a menu → Stack or Drawer screen.
Two lookalikes are components rather than routes: horizontal tabs inside the content area (`react-native-tab-view`, or a plain segmented control), and a floating action button (an absolutely positioned `Pressable`).
Tab bar and header styling go through `screenOptions` for as long as the design stays inside what those options express; a custom shape, a floating or pill-shaped bar, a raised center button, a custom active indicator or a header carrying a logo and search field is cheaper as a component passed to the `tabBar` prop or `options.header`. Either way, match the icons, labels and active/inactive states from the design.
### Keyboard
Every screen with a `TextInput` needs keyboard avoidance, such as `KeyboardAvoidingView`. Verify the focused fields, errors and form actions remain reachable with the native keyboard open.
### Build the feature, not the mockup
A mockup shipped verbatim is a convincing dead app. For each screen, name the feature the UI represents, then wire it: every value that would change at runtime reads from state, props, context or the API; buttons run actions; forms validate and submit; lists fetch; navigation carries params between screens. The hardcoded name, the "5", the highlighted tab and the pre-filled form are all the same job.
A mockup also shows one moment — full, happy, populated. Decide separately what each screen does with zero items, with one, while loading, and on error.
When editing a persisted record, resolve it from the route parameters and initialize fields after its data loads; test reopening and reloading the route so saved fields do not become blank defaults.
Preserve small icons visually while giving their controls usable touch areas.
If the app also targets web, verify that inactive routes and covered modal content cannot receive keyboard focus; `aria-hidden` alone does not prevent it.
---
## Definition of done
For a native build, apply each relevant check to every screen in the requested implementation scope and any shared navigation it changes. Completion requires those checks to pass; report unavailable platform checks as unverified dependencies.
- [ ] The screen has a route in the navigator, and its own file draws none of the chrome the navigator owns.
- [ ] Every `flex` in the source HTML resolved to an explicit direction, and the built screen's axis matches its screenshot.
- [ ] Review JSX text and conditional branches: every rendered string is inside `<Text>`. Exercise populated and empty branches on native and check the development console for `Text strings must be rendered within a <Text> component.`; grep alone cannot prove the parent element.
- [ ] Headerless screens and modals inset their uncovered edges, every screen's bottom edge is accounted for on Android, the applicable native view hierarchy has a `SafeAreaProvider`, and nothing is inset twice.
- [ ] Colors, radii and fonts come from the one token module — grepping the screen files for a raw hex or `rgb(` returns nothing.
- [ ] Icons are the exact Iconify names from the HTML, set and name.
- [ ] Fonts match the selected theme's families, bundled at the weights the body's `font-*` classes use.
- [ ] Shadows render on Android as well as iOS: `boxShadow`, or `elevation` alongside the `shadow*` props on a legacy-architecture project.
- [ ] Gradients visibly match the design on native through `expo-linear-gradient` or a supported native gradient implementation.
- [ ] Every `hover:` in the source HTML landed on a press state: `Pressable`'s `({ pressed })` callback or `active:`.
- [ ] Every screen holding a `TextInput` uses keyboard avoidance. With the native keyboard open, focus each field and trigger validation: errors and Save/Cancel remain visible or reachable by scrolling. A `KeyboardAvoidingView` in the source alone does not prove this.
- [ ] Grep each screen for display literals — quoted strings and numbers that reach the user — and account for every hit: it reads from state, props, context or the API, or it is genuinely static copy.
- [ ] Exercise applicable populated, empty, single-item, loading and error states. On native, check that filtering to one item does not unexpectedly expand scroll containers or hide the remaining item.
- [ ] When using Nativewind or Uniwind, one throwaway class renders visibly before building screens, including a third-party wrapper if the implementation needs one.
- [ ] The screen has been compared against its own **review shot**, not the user shot.
- [ ] Record which platforms actually ran the app and which only bundled/exported. Verify styling and interactions on an available native target; a web preview or native export alone does not prove native layout, insets or keyboard behavior.
SKILL.md›
---
name: design-mobile-apps
description: Design mobile app screens with Sleek, edit Sleek projects, and implement their designs in React Native or HTML.
metadata:
requires-env: SLEEK_API_KEY
---
# Designing with Sleek
[](https://sleek.design)
## Overview
[sleek.design](https://sleek.design) provides a REST API at `/api/v1/*` to create projects, design screens from a brief, and fetch their HTML and renders.
**Base URL**: `https://sleek.design`
**Auth**: `Authorization: Bearer $SLEEK_API_KEY`, except the public device start/poll endpoints
**Content-Type**: send `application/json` for JSON request bodies.
**CORS**: Enabled on all `/api/v1/*` endpoints
**Envelope**: check HTTP status first. Successful JSON wraps its payload in `data`; lists also include sibling `pagination`. Errors use top-level `code`, `status` and `message`, with optional `data.url`. Successful screenshots and share cards return image bytes; `204` has no body.
**Ids**: opaque 11-character tokens like `V1StGXR8Z5j`, with no type prefix. Nothing about an id says whether it names a project, screen, component, version or theme, so track each one by the response field it arrived in.
**Parsing responses**: write the body to a file (`curl -o run.json`) and parse the file. Piping JSON through `echo` corrupts it: zsh expands the escaped `\n` inside string values into real newlines, which makes the body invalid JSON.
**API docs**: OpenAPI spec at `https://sleek.design/api/v1/spec.json`; browsable docs at `https://sleek.design/api/v1/docs`. Fetch the spec for any contract detail not covered here.
---
## Designing or implementing
A single run can take both directions — "design me a fitness app and build it in Expo" is a design pass followed by an implementation pass. Gate each pointer on the artefact in front of you rather than on a one-time reading of the request:
**Designing** — you are about to send a chat message to Sleek. Read [designing.md](designing.md) first; it carries the create → chat → screenshot loop, how to author a style direction, and when to seed one from a reference instead.
**Implementing.** Before writing code from a Sleek design, read [implementing.md](implementing.md) for version selection, native conversion, working features, and acceptance checks.
## User shot and review shot
Screenshots come in two framings, and each file below asks for one by name:
- A **user shot** is the default render: viewport height, phone-shaped. This is the one you show the user.
- A **review shot** adds `fullHeight: true`, one component per request. This is the one you judge your own work against.
---
## Prerequisites: API Key
If `SLEEK_API_KEY` is not set, use the device flow so the user never handles the raw key:
1. `POST https://sleek.design/api/v1/device/start` (no auth) with body `{"source": "your-tool-slug"}`. The response contains a `verificationUrl`, a human-checkable `userCode`, a secret `deviceCode`, and a poll `interval` in seconds.
2. Show the user the `verificationUrl` and the `userCode`, and tell them to confirm the code matches before approving.
3. Poll `POST https://sleek.design/api/v1/device/poll` with `{"deviceCode": "..."}` every `interval` seconds. On `pending`, keep waiting. On `approved`, store `data.key` privately as `SLEEK_API_KEY` on that first read: the key is handed over once. On `expired` or `404 NOT_FOUND`, start a new flow. See [Device: Poll](endpoints.md#device-poll) for the response contract.
Fallback: send the user to **https://sleek.design/agents/setup**, which handles sign-in, plan upgrade, and key creation in one place, and ask them to paste the key back to you. Keys can also be managed at **https://sleek.design/dashboard/api-keys**. The full key value is shown only once at creation, and a free account holds one active key at a time.
**Plans**: free accounts can try the API with their one-time trial credits (about one design run), so a new user can see their first design before any payment decision. Sustained use requires the Pro plan or higher. This file carries no figures on purpose — read the current price, billing periods and credit allowance from **https://sleek.design/agents.md**, which is generated from Sleek's live pricing, and quote them from there. When cost becomes relevant (the user asks, an upgrade is needed to continue, or you're about to send them to a payment page), state that pricing plainly, including the yearly option, so the user sees a payment step coming before they reach it.
---
## Quick Reference: All Endpoints
Every endpoint the API exposes:
| Method | Path | Scope |
| -------- | ---------------------------------------------- | ----------------- |
| `GET` | `/api/v1/projects` | `projects:read` |
| `POST` | `/api/v1/projects` | `projects:write` |
| `GET` | `/api/v1/projects/:id` | `projects:read` |
| `DELETE` | `/api/v1/projects/:id` | `projects:write` |
| `GET` | `/api/v1/projects/:id/components` | `components:read` |
| `GET` | `/api/v1/projects/:id/components/:componentId` | `components:read` |
| `GET` | `/api/v1/references` | any valid key |
| `POST` | `/api/v1/projects/:id/chat/messages` | `chats:write` |
| `GET` | `/api/v1/projects/:id/chat/runs/:runId` | `chats:read` |
| `POST` | `/api/v1/projects/:id/chat/runs/:runId/cancel` | `chats:write` |
| `POST` | `/api/v1/screenshots` | `screenshots` |
| `POST` | `/api/v1/share-cards` | `screenshots` |
| `POST` | `/api/v1/device/start` | public, no auth |
| `POST` | `/api/v1/device/poll` | public, no auth |
Create a key holding only the scopes the task uses.
Every row above is specified in full — request body, response shape, field defaults, status codes — in [endpoints.md](endpoints.md). This table is enough to pick an endpoint; read the reference when you need a contract detail it doesn't carry.
---
## Error Shapes
```json
{
"defined": true,
"code": "FORBIDDEN",
"status": 403,
"message": "...",
"data": { "url": "https://sleek.design/..." }
}
```
| HTTP | Code | When |
| ---- | ----------------------- | ------------------------------------------------------------------------ |
| 401 | `UNAUTHORIZED` | Missing/invalid/expired API key |
| 403 | `FORBIDDEN` | Valid key, wrong scope or plan |
| 404 | `NOT_FOUND` | Resource doesn't exist |
| 400 | `BAD_REQUEST` | Validation failure |
| 409 | `CONFLICT` | Another run is active for this project |
| 429 | `TOO_MANY_REQUESTS` | Render cap: 1000 screenshots + share cards per hour per org, counted on API-key requests only |
| 500 | `INTERNAL_SERVER_ERROR` | Server error |
`401`, `403` and `429` bodies may include `data.url`: a page where the user can fix the condition (create a key, upgrade the plan). Share it as-is.
Chat run-level errors (inside `data.error`):
| Code | Meaning |
| --------------------------------- | ---------------------------------------------------------- |
| `out_of_credits` | Organization has no credits left |
| `execution_failed` | AI execution error |
| `cancelled` | Run cancelled via the cancel endpoint |
| `request_message_persist_failed` | The run failed before reaching the AI; the message was never stored |
For `out_of_credits`, relay `error.url` and wait for the user to resolve it. Follow [Safe retries](designing.md#2-send-a-chat-message) for replay versus a new attempt after either recoverable failure.
styling/nativewind.md›
# Styling: Nativewind
Tailwind classes on React Native primitives: same elements, `className` in place of a style object, so the mockup's classes carry over almost verbatim. Reached from [React Native (Expo)](../implementing.md#react-native-expo) when `nativewind` is in `package.json`.
## Check the installed major first
The two Nativewind lines consume different Tailwind majors, and everything below branches on which one is installed:
| Installed Nativewind | Tailwind it expects | Sleek's classes | `oklch(...)` tokens |
| -------------------- | ------------------- | --------------- | -------------------------------------------------------------- |
| v4 | v3 | convert v4 → v3 | convert to hex; `react-native-css-interop` rejects `oklch` |
| v5 | v4 | use as-is | keep as `oklch`; `react-native-css` converts them at build time |
Read the resolved version from the lockfile and check registry tags when installing; choose prereleases deliberately. For v5, use the matched Nativewind/`react-native-css` pair from the [current installation guide](https://www.nativewind.dev/v5/getting-started/installation). Use Expo's installer for compatible native peers and check dependency minimums in the guide for the selected major.
## Tailwind v4 → v3, for the constructs Sleek emits
| Sleek (Tailwind v4) | Nativewind v4 (Tailwind v3) |
| -------------------------- | ------------------------------------------------ |
| `shadow-xs`, `shadow-sm` | `shadow-sm`, `shadow` |
| `rounded-xs`, `rounded-sm` | `rounded-sm`, `rounded` |
| `outline-hidden` | `outline-none` |
| `bg-linear-to-b` | `bg-gradient-to-b` (then `expo-linear-gradient`) |
| `bg-(--token)` | `bg-[var(--token)]`, or a config color |
A class that resolves to nothing is usually one of these renames. Under v5 none of it applies.
## Tokens
`@theme` / `@theme inline` is v4 syntax with no v3 equivalent: under Nativewind v4 the tokens go into `theme.extend` in `tailwind.config.js`, which is what makes `bg-primary` and `text-foreground` resolve at all. Under v5 the `@theme` block ports to the CSS entry as it stands.
## Configuration
A missing build step presents as every class doing nothing at all, **silently**. When the app renders unstyled, check this list before rewriting classes — and check the list for the major you actually have, because they differ:
**v4** — the Babel preset, the `withNativeWind` Metro wrapper with its CSS `input`, `tailwind.config.js`, and the CSS entry imported in the root layout. Use the [v4 installation guide](https://www.nativewind.dev/docs/getting-started/installation) for the installed SDK.
**v5** — the matched CSS engine, Tailwind/PostCSS, Expo-compatible Reanimated/Worklets and safe-area peers; `postcss.config.mjs` loading `@tailwindcss/postcss`; the `withNativewind` Metro wrapper; CSS imported once in the root layout; and the Lightning CSS pin prescribed by the current guide, using the package manager's actual override syntax. Keep `babel-preset-expo`; remove v4's Nativewind preset and JSX import-source configuration.
For the current v5 CSS entry, keep utilities unlayered so React Native Web defaults do not override them:
```css
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/preflight.css" layer(base);
@import "tailwindcss/utilities.css";
@import "nativewind/theme";
```
## Units
Sleek's default web `rem` is 16px; Nativewind's native default is 14. Check the HTML's root size and align it before copying classes, or spacing and typography shrink on the device despite matching on web. V4 accepts `inlineRem: 16` in its Metro wrapper. For v5 use the CSS root font size supported by its current engine, such as `:root { font-size: 16px; }`, and verify a measured native class rather than assuming a v4 Metro option carries across. See [v5 units](https://www.nativewind.dev/v5/core-concepts/units).
For a class-styled `Pressable`, verify dimensions from a `style` callback on native too. If callback styles are ignored, keep fixed sizes in classes or static styles and use `active:` classes for press feedback. A rendered color/spacing probe does not verify this separate path.
## Third-party components
Before passing `className` to a third-party component such as `LinearGradient`, check whether it forwards classes to a supported primitive or needs an explicit mapping. V4 uses [cssInterop](https://www.nativewind.dev/docs/api/css-interop) when styles must be resolved; v5 uses a returned [styled wrapper](https://www.nativewind.dev/v5/guides/third-party-components). Define mappings once outside render and use the mapped component. Include it in the native styling probe.
styling/stylesheet.md›
# Styling: StyleSheet
React Native's built-in styling. Reached from [React Native (Expo)](../implementing.md#react-native-expo) when the repo has no styling library.
## Colors
React Native's color parser reads hex, `rgb()` and named colors and carries no `oklch` matcher, so a raw `oklch(...)` token resolves to nothing. Convert each one from the document's `:root` block to hex or `rgb()` once, in the token module, and every consumer stays correct.
## Units
React Native styles use unitless numbers in density-independent pixels. Resolve Tailwind's `0.25rem` spacing steps against the HTML's root font size; the examples below assume the default 16px root:
| Class | Style |
| -------------- | --------------------------------- |
| `p-4` | `padding: 16` |
| `px-5 py-3` | `paddingHorizontal: 20, paddingVertical: 12` |
| `gap-3` | `gap: 12` |
| `text-sm` | `fontSize: 14` |
| `rounded-xl` | `borderRadius:` the resolved `--radius-xl` number |
| `w-1/2` | `width: '50%'` (percentages are strings) |
Resolve `rem` and `em` against their source font sizes. Use `useWindowDimensions()` for viewport-relative sizes, then pass numeric native values.
## Composition
- One `StyleSheet.create({ … })` per component file, holding everything static.
- Conditional styling composes with arrays: `style={[styles.card, isActive && styles.cardActive]}`.
- Values that genuinely change per render — a measured width, an animated height — stay inline; everything else lives in the sheet.
styling/uniwind.md›
# Styling: Uniwind
Uniwind consumes Tailwind v4, so the mockup's classes carry over unchanged. Reached from [React Native (Expo)](../implementing.md#react-native-expo) when `uniwind` is in `package.json`.
## Theme
Uniwind resolves theme colors from `global.css`. Read the project's existing `global.css` and follow the structure already there. Sleek's own React Native export writes this shape, which is worth confirming against the current docs before relying on it:
```css
@import "tailwindcss";
@import "uniwind";
@layer theme {
:root {
@variant dark {
--color-background: #000000;
--color-foreground: #ffffff;
}
@variant light {
--color-background: #ffffff;
--color-foreground: #000000;
}
}
}
```
Port the project's `:root` tokens into `--color-*` names in whatever shape that file actually uses. Uniwind documents first-class OKLCH support, so the `oklch(...)` values go across as they are. Uniwind publishes an agent-readable digest at `https://docs.uniwind.dev/llms.txt`; read it for the current theme structure and for any setup detail beyond this file.
## Third-party components
Components from other libraries — `SafeAreaView` from `react-native-safe-area-context`, for instance — accept `className` once wrapped. Wrap each one once, in its own file, and import the wrapped version everywhere:
```tsx
import { SafeAreaView } from "react-native-safe-area-context";
import { withUniwind } from "uniwind";
export const StyledSafeAreaView = withUniwind(SafeAreaView);
```